From 0c37d856b53220c001958679c779c5e3d404c142 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Sat, 7 Mar 2026 22:39:10 +0100 Subject: [PATCH 01/16] package, imports, class header --- besser/generators/spring/__init__.py | 1 + .../generators/spring/generated/Computer.java | 11 + besser/generators/spring/generated/Thing.java | 11 + besser/generators/spring/generated/User.java | 11 + besser/generators/spring/generator.py | 64 ++ besser/generators/spring/proba.py | 49 ++ .../spring/spring_entity_generator.py | 745 ++++++++++++++++++ .../generators/spring/templates/__init__.py | 0 besser/generators/spring/templates/entity.j2 | 15 + .../spring/templates/entity.java.j2 | 61 ++ .../spring/templates/entity_template.j2 | 314 ++++++++ .../generators/spring/templates/enum.java.j2 | 9 + 12 files changed, 1291 insertions(+) create mode 100644 besser/generators/spring/__init__.py create mode 100644 besser/generators/spring/generated/Computer.java create mode 100644 besser/generators/spring/generated/Thing.java create mode 100644 besser/generators/spring/generated/User.java create mode 100644 besser/generators/spring/generator.py create mode 100644 besser/generators/spring/proba.py create mode 100644 besser/generators/spring/spring_entity_generator.py create mode 100644 besser/generators/spring/templates/__init__.py create mode 100644 besser/generators/spring/templates/entity.j2 create mode 100644 besser/generators/spring/templates/entity.java.j2 create mode 100644 besser/generators/spring/templates/entity_template.j2 create mode 100644 besser/generators/spring/templates/enum.java.j2 diff --git a/besser/generators/spring/__init__.py b/besser/generators/spring/__init__.py new file mode 100644 index 000000000..4f77175d7 --- /dev/null +++ b/besser/generators/spring/__init__.py @@ -0,0 +1 @@ +from .spring_entity_generator import * \ No newline at end of file diff --git a/besser/generators/spring/generated/Computer.java b/besser/generators/spring/generated/Computer.java new file mode 100644 index 000000000..806781d58 --- /dev/null +++ b/besser/generators/spring/generated/Computer.java @@ -0,0 +1,11 @@ +package spring.entities; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.Id; + +@Entity +@Table(name="computers") +public class Computer extends Thing { + +} \ No newline at end of file diff --git a/besser/generators/spring/generated/Thing.java b/besser/generators/spring/generated/Thing.java new file mode 100644 index 000000000..053376e53 --- /dev/null +++ b/besser/generators/spring/generated/Thing.java @@ -0,0 +1,11 @@ +package spring.entities; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.Id; +import javax.persistence.MappedSuperclass; + +@MappedSuperclass +public abstract class Thing { + +} \ No newline at end of file diff --git a/besser/generators/spring/generated/User.java b/besser/generators/spring/generated/User.java new file mode 100644 index 000000000..a38c620fb --- /dev/null +++ b/besser/generators/spring/generated/User.java @@ -0,0 +1,11 @@ +package spring.entities; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.Id; + +@Entity +@Table(name="users") +public class User { + +} \ No newline at end of file diff --git a/besser/generators/spring/generator.py b/besser/generators/spring/generator.py new file mode 100644 index 000000000..32ad275d9 --- /dev/null +++ b/besser/generators/spring/generator.py @@ -0,0 +1,64 @@ +import os +from typing import List +from jinja2 import Environment, FileSystemLoader +from besser.BUML.metamodel.structural import DomainModel +from besser.BUML.metamodel.structural.structural import Class +from besser.generators import GeneratorInterface +from besser.utilities.utils import sort_by_timestamp + + +class SpringEntityGenerator(GeneratorInterface): + + def __init__(self, model: DomainModel, + output_dir: str = "./generated/entities", + package_name: str = "com.example.entities"): + super().__init__(model, output_dir) + + self.package_name: str = package_name + + def generate(self): + model: DomainModel = self.model + + for cls in model.classes_sorted_by_inheritance(): + self._generate_class_file(cls) + + + def _generate_class_file(self, cls: Class): + file_path = self.build_generation_path(file_name=f"{cls.name.capitalize()}.java") + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path)) + entity_template = env.get_template("entity.j2") + + imports: List[str] = self._get_default_imports() + + if cls.is_abstract: + imports.append("javax.persistence.MappedSuperclass") + + context = { + "cls": cls, + "package_name": self.package_name, + "imports": imports, + "is_abstract": cls.is_abstract, + "table_name": self._pluralize(cls.name.lower()), + "parent": cls.parents().pop().name if cls.parents() else None + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = entity_template.render(**context) + f.write(generated_code) + + def _get_default_imports(self) -> List[str]: + return [ + "jakarta.persistence.Entity", + "jakarta.persistence.Table", + "jakarta.persistence.Id", + ] + + @staticmethod + def _pluralize(name: str) -> str: + if name.endswith("y"): + return name[:-1] + "ies" + elif name.endswith(("s", "x", "z", "ch", "sh")): + return name + "es" + else: + return name + "s" diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py new file mode 100644 index 000000000..e973699c6 --- /dev/null +++ b/besser/generators/spring/proba.py @@ -0,0 +1,49 @@ +""" +Jednostavna proba Spring Entity Generatora - sa apsolutnom putanjom +""" + +import sys +import os +from pathlib import Path +import tempfile +import shutil + +# Dodaj root direktorijum BESSER-a +# root_dir = Path(__file__).parent +# sys.path.insert(0, str(root_dir)) + +# Uvezi sa eksplicitnom putanjom +from besser.BUML.metamodel.structural.structural import ( + DomainModel, Class, Generalization, Property, Type, Association, Enumeration, EnumerationLiteral +) +from besser.generators.spring.generator import SpringEntityGenerator + + +def proba_jednostavna_klasa(): + current_dir = Path(__file__).parent + output_dir = current_dir / "generated" + output_dir.mkdir(exist_ok=True) + + user = Class(name="User") + user.add_attribute(Property(name="email", type=Type("String"))) + user.add_attribute(Property(name="firstName", type=Type("String"))) + + thing = Class(name="Thing", is_abstract=True) + + computer = Class(name="Computer") + computer.add_attribute(Property(name="name", type=Type("String"))) + + gen1 = Generalization(general=thing, specific=computer) + + model = DomainModel(name="proba", types={user, thing, computer}, generalizations={gen1}) + + generator = SpringEntityGenerator( + model, + output_dir=output_dir, + package_name="spring.entities" + ) + + files = generator.generate() + +if __name__ == "__main__": + proba_jednostavna_klasa() \ No newline at end of file diff --git a/besser/generators/spring/spring_entity_generator.py b/besser/generators/spring/spring_entity_generator.py new file mode 100644 index 000000000..ce687a33c --- /dev/null +++ b/besser/generators/spring/spring_entity_generator.py @@ -0,0 +1,745 @@ +""" +Spring Entity Code Generator for BESSER B-UML Models +Generates Spring JPA Entity classes from BESSER structural models +With full support for JPA/Hibernate associations and BESSER metamodel features + +BESSER dokumentacija: +https://besser.readthedocs.io/en/latest/api/BUML/metamodel/api_structural.html + +Features: +- Apstraktne klase (abstract classes) +- Enumeracije (enumerations) +- ID atributi (@Id annotation) +- Navigable fields +- Opcioni atributi +- Visibility modifiers (public, private, protected) +- Readonly atributi (@Immutable) +- Izvedeni atributi (derived) +- Updatable/Insertable constraints +- Opisi (descriptions) i metadata +- Unique constraints +- Inheritance strategies + +Author: Generator for BESSER-PEARL/BESSER +""" + +from typing import Set, Dict, List, Optional +from pathlib import Path +import re +from enum import Enum +from jinja2 import Template, Environment, FileSystemLoader + +from besser.BUML.metamodel.structural.structural import ( + Class, Association, DomainModel, Property, Type, Multiplicity, Enumeration +) +from besser.generators.generator_interface import GeneratorInterface + + +class SpringEntityGenerator(GeneratorInterface): + """ + Generator za Spring Entity klase iz BESSER B-UML modela. + + Podržava sve BESSER metamodel koncepte: + - Apstraktne klase + - Enumeracije + - ID atributi + - Navigable polja + - Opcioni atributi + - Visibility modifiers + - Readonly atributi + - Izvedeni atributi + - Updatable/Insertable constraints + - Inheritance + - Sva JPA/Hibernate asocijacije + + Primer korišćenja: + ```python + from besser.BUML.metamodel.structural.structural import Diagram + from spring_entity_generator import SpringEntityGenerator + + diagram = Diagram() + # ... dodaj klase, atribute, relacije ... + + generator = SpringEntityGenerator( + diagram, + output_path="./src/main/java", + templates_path="./templates" + ) + generator.generate() + ``` + """ + + # Java tipovi mapiranje + JAVA_TYPE_MAPPING = { + 'String': 'String', + 'Integer': 'Integer', + 'Int': 'Integer', + 'Long': 'Long', + 'Double': 'Double', + 'Float': 'Float', + 'Boolean': 'Boolean', + 'Bool': 'Boolean', + 'LocalDate': 'LocalDate', + 'LocalDateTime': 'LocalDateTime', + 'Date': 'LocalDateTime', + 'BigDecimal': 'BigDecimal', + 'UUID': 'UUID', + } + + def __init__(self, model, output_path: str = "./generated/entities", + package_name: str = "com.example.entity", + templates_path: str = "./templates"): + """ + Inicijalizuj generator + + Args: + model: B-UML Diagram model + output_path: Putanja gde će biti sačuvane generirane klase + package_name: Java package za generirane klase + templates_path: Putanja do Jinja2 template fajlova + """ + super().__init__(model) + self.output_path = Path(output_path) + self.package_name = package_name + self.templates_path = Path(templates_path) + self.output_path.mkdir(parents=True, exist_ok=True) + + # Inicijalizuj Jinja2 okruženje + self.jinja_env = Environment( + loader=FileSystemLoader(str(self.templates_path)), + trim_blocks=True, + lstrip_blocks=True + ) + + # Mapiranje B-UML klasa sa njihovim asocijacijama + self.associations_by_class: Dict[str, List[Association]] = {} + self.enumerations: Dict[str, Enumeration] = {} + + self._build_association_map() + self._build_enumerations_map() + + def _build_association_map(self): + """Gradi mapu asocijacija za svaku klasu""" + if not hasattr(self.model, 'classes') or self.model.classes is None: + return + + for cls in self.model.classes: + self.associations_by_class[cls.name] = [] + + for cls in self.model.classes: + if hasattr(cls, 'association_ends') and cls.association_ends: + for assoc_end in cls.association_ends: + if hasattr(assoc_end, 'association'): + assoc = assoc_end.association + if assoc not in self.associations_by_class[cls.name]: + self.associations_by_class[cls.name].append(assoc) + + def _build_enumerations_map(self): + """Gradi mapu enumeracija""" + if not hasattr(self.model, 'enumerations') or self.model.enumerations is None: + return + + for enum in self.model.enumerations: + self.enumerations[enum.name] = enum + + def generate(self) -> List[str]: + """ + Generiši sve Spring Entity klase i enumeracije + + Returns: + Lista putanja generianih datoteka + """ + generated_files = [] + + # Generiši enumeracije + if hasattr(self.model, 'enumerations') and self.model.enumerations: + for enum in self.model.enumerations: + try: + file_path = self._generate_enumeration_class(enum) + generated_files.append(file_path) + except Exception as e: + print(f"Greška pri generisanju enumeracije {enum.name}: {e}") + + # Generiši entitete + if not hasattr(self.model, 'classes') or self.model.classes is None: + return generated_files + + for cls in self.model.classes: + try: + file_path = self._generate_entity_class(cls) + generated_files.append(file_path) + except Exception as e: + print(f"Greška pri generisanju klase {cls.name}: {e}") + + return generated_files + + def _generate_enumeration_class(self, enum: Enumeration) -> str: + """Generiši Java enumeraciju""" + enum_values = [] + + if hasattr(enum, 'literals') and enum.literals: + for literal in enum.literals: + enum_values.append(literal.name if hasattr(literal, 'name') else str(literal)) + + enum_code = f"""package {self.package_name}; + +/** + * Enumeracija {enum.name} + * + * Generirano automatski od BESSER B-UML modela + * + * @author Spring Entity Generator + * @version 1.0 + */ +public enum {enum.name} {{ +""" + + for i, value in enumerate(enum_values): + enum_code += f" {value}" + if i < len(enum_values) - 1: + enum_code += ",\n" + else: + enum_code += ";\n" + + enum_code += f""" + /** + * Pronađi enumeraciju po imenu + * + * @param value String vrednost + * @return {enum.name} enumeracija + */ + public static {enum.name} fromValue(String value) {{ + try {{ + return valueOf(value.toUpperCase()); + }} catch (IllegalArgumentException e) {{ + return null; + }} + }} +}} +""" + + # Napiši datoteku + file_path = self.output_path / f"{enum.name}.java" + with open(file_path, 'w', encoding='utf-8') as f: + f.write(enum_code) + + return str(file_path) + + def _generate_entity_class(self, buml_class: Class) -> str: + """Generiši jednu Spring Entity klasu""" + + # Pripremi podatke za template + context = { + 'class_name': buml_class.name, + 'package_name': self.package_name, + 'table_name': self._to_snake_case(buml_class.name), + 'parent': self._get_parent_class(buml_class), + 'is_abstract': self._is_abstract_class(buml_class), + 'description': self._get_description(buml_class), + 'imports': self._generate_imports(buml_class), + 'attributes': self._prepare_attributes(buml_class), + 'relations': self._prepare_relations(buml_class), + 'unique_constraints': self._prepare_unique_constraints(buml_class), + 'constructor_params': self._generate_constructor_params(buml_class), + 'constructor_params_list': self._generate_constructor_params_list(buml_class), + 'constructor_assignments': self._generate_constructor_assignments(buml_class), + 'getters_setters': self._prepare_getters_setters(buml_class), + } + + # Učitaj i primeni template + template = self.jinja_env.get_template('entity_template.j2') + entity_code = template.render(**context) + + # Napiši datoteku + file_path = self.output_path / f"{buml_class.name}.java" + with open(file_path, 'w', encoding='utf-8') as f: + f.write(entity_code) + + return str(file_path) + + def _is_abstract_class(self, buml_class: Class) -> bool: + """Proverite da li je klasa apstraktna""" + return hasattr(buml_class, 'is_abstract') and buml_class.is_abstract + + def _get_parent_class(self, buml_class: Class) -> Optional[str]: + """Pronađi parent klasu ako postoji generalizacija""" + if hasattr(buml_class, 'parents') and buml_class.parents: + parents = buml_class.parents + if parents: + parent = list(parents)[0] + return parent.name + return None + + def _get_description(self, buml_class: Class) -> Optional[str]: + """Preuzmi opis klase""" + if hasattr(buml_class, 'metadata') and buml_class.metadata: + if hasattr(buml_class.metadata, 'description'): + return buml_class.metadata.description + return None + + def _get_visibility(self, element) -> str: + """Preuzmi visibility modifier""" + visibility = "private" # Default + + if hasattr(element, 'visibility'): + vis = element.visibility + if vis is None: + visibility = "public" + elif isinstance(vis, str): + vis_lower = vis.lower() + if 'public' in vis_lower: + visibility = "public" + elif 'protected' in vis_lower: + visibility = "protected" + elif 'private' in vis_lower: + visibility = "private" + else: + # Ako je VisibilityKind enum + try: + if hasattr(vis, 'name'): + vis_name = vis.name.lower() + if 'public' in vis_name: + visibility = "public" + elif 'protected' in vis_name: + visibility = "protected" + elif 'private' in vis_name: + visibility = "private" + except: + pass + + return visibility + + def _generate_imports(self, buml_class: Class) -> List[str]: + """Generiši potrebne Java importe""" + imports = set([ + "jakarta.persistence.Entity", + "jakarta.persistence.Table", + "jakarta.persistence.Id", + "jakarta.persistence.GeneratedValue", + "jakarta.persistence.GenerationType", + "jakarta.persistence.Column", + "jakarta.persistence.OneToOne", + "jakarta.persistence.OneToMany", + "jakarta.persistence.ManyToOne", + "jakarta.persistence.ManyToMany", + "jakarta.persistence.JoinColumn", + "jakarta.persistence.JoinTable", + "jakarta.persistence.FetchType", + "jakarta.persistence.CascadeType", + "jakarta.persistence.Enumerated", + "jakarta.persistence.EnumType", + "jakarta.persistence.UniqueConstraint", + "java.time.LocalDate", + "java.time.LocalDateTime", + "java.util.HashSet", + "java.util.Set", + "java.util.Objects", + ]) + + # Dodaj inheritance import ako je potrebna + if self._is_abstract_class(buml_class): + imports.add("jakarta.persistence.Inheritance") + imports.add("jakarta.persistence.InheritanceType") + + # Dodaj importe na osnovu atributa + if buml_class.attributes: + for attr in buml_class.attributes: + if self._is_readonly_attribute(attr): + imports.add("org.hibernate.annotations.Immutable") + + if self._is_enum_attribute(attr): + enum_type = self._get_enum_name(attr.type) + imports.add(f"{self.package_name}.{enum_type}") + elif hasattr(attr.type, 'name'): + type_name = attr.type.name + if 'BigDecimal' in type_name: + imports.add("java.math.BigDecimal") + if 'UUID' in type_name: + imports.add("java.util.UUID") + + return sorted(list(imports)) + + def _is_readonly_attribute(self, attr: Property) -> bool: + """Proverite da li je atribut readonly""" + return hasattr(attr, 'is_read_only') and attr.is_read_only + + def _is_derived_attribute(self, attr: Property) -> bool: + """Proverite da li je atribut izvedena vrednost""" + return hasattr(attr, 'is_derived') and attr.is_derived + + def _is_enum_attribute(self, attr: Property) -> bool: + """Proverite da li je atribut enumeracija""" + if hasattr(attr, 'type') and attr.type: + type_name = attr.type.name if hasattr(attr.type, 'name') else str(attr.type) + return type_name in self.enumerations + return False + + def _get_enum_name(self, attr_type) -> str: + """Preuzmi naziv enumeracije""" + if hasattr(attr_type, 'name'): + return attr_type.name + return str(attr_type) + + def _prepare_attributes(self, buml_class: Class) -> List[Dict]: + """Pripremi podatke za atribute""" + attributes = [] + + if not buml_class.attributes: + return attributes + + for attr in buml_class.attributes: + if self._is_association_end(attr): + continue + + # Proverite da li je ovo ID polje + is_id = hasattr(attr, 'is_id') and attr.is_id + + # Visibility + visibility = self._get_visibility(attr) + + # Readonly + is_readonly = self._is_readonly_attribute(attr) + + # Derived + is_derived = self._is_derived_attribute(attr) + + # Enumeracija + is_enum = self._is_enum_attribute(attr) + + java_type = self._map_type_to_java(attr.type) + column_name = self._to_snake_case(attr.name) + + # Nullable + nullable = True + if hasattr(attr, 'lower') and attr.lower == 1: + nullable = False + + # Length + length = None + if java_type == 'String': + length = 255 + + # Unique + unique = False + if 'email' in attr.name.lower() or 'username' in attr.name.lower(): + unique = True + + # Opis + description = None + if hasattr(attr, 'metadata') and attr.metadata: + if hasattr(attr.metadata, 'description'): + description = attr.metadata.description + + # Default vrednost + default_value = None + if hasattr(attr, 'default_value'): + default_value = attr.default_value + + # Updatable/Insertable + updatable = None + insertable = None + + if is_readonly: + updatable = False + + if is_id or is_derived: + insertable = False + + # Enum vrednosti + enum_values = None + if is_enum: + enum_name = self._get_enum_name(attr.type) + if enum_name in self.enumerations: + enum = self.enumerations[enum_name] + if hasattr(enum, 'literals'): + enum_values = [lit.name if hasattr(lit, 'name') else str(lit) for lit in enum.literals] + + attributes.append({ + 'name': attr.name, + 'java_type': java_type, + 'column_name': column_name, + 'visibility': visibility, + 'nullable': nullable, + 'unique': unique, + 'length': length, + 'is_id': is_id, + 'is_enum': is_enum, + 'is_readonly': is_readonly, + 'is_derived': is_derived, + 'description': description, + 'default_value': default_value, + 'updatable': updatable, + 'insertable': insertable, + 'enum_values': ", ".join(enum_values) if enum_values else None + }) + + return attributes + + def _prepare_relations(self, buml_class: Class) -> List[Dict]: + """Pripremi podatke za relacije""" + relations = [] + + if buml_class.name not in self.associations_by_class: + return relations + + for assoc in self.associations_by_class[buml_class.name]: + source_end = None + target_end = None + + if hasattr(assoc, 'ends') and assoc.ends: + for end in assoc.ends: + if hasattr(end, 'type') and end.type == buml_class: + source_end = end + else: + target_end = end + + if not source_end or not target_end: + continue + + relation_type = self._determine_relation_type(source_end, target_end) + target_class = target_end.type.name if hasattr(target_end, 'type') else "Unknown" + field_name = self._to_camel_case_first_lower(target_class) + plural_name = self._pluralize(field_name) + plural_name_capitalized = self._capitalize_first(plural_name) + + # Visibility + visibility = self._get_visibility(source_end) + + # Navigable + is_navigable = hasattr(source_end, 'is_navigable') and source_end.is_navigable + + # Owning side + is_owning_side = relation_type in ["ManyToOne", "ManyToMany"] + + # Cascade types + cascade_types = ["PERSIST", "MERGE"] + if relation_type == "OneToMany": + cascade_types = ["ALL"] + + # Fetch type + fetch_type = "LAZY" + + # Optional + is_optional = True + if relation_type in ["ManyToOne"]: + if hasattr(target_end, 'lower') and target_end.lower == 1: + is_optional = False + + # Opis + description = None + if hasattr(source_end, 'metadata') and source_end.metadata: + if hasattr(source_end.metadata, 'description'): + description = source_end.metadata.description + + side_description = "OWNING SIDE" if is_owning_side else "INVERSE SIDE" + cascade_types_str = ", ".join(cascade_types) + + relation_data = { + 'type': relation_type, + 'target_class': target_class, + 'field_name': field_name, + 'plural_name': plural_name, + 'plural_name_capitalized': plural_name_capitalized, + 'join_column_name': self._to_snake_case(field_name) + '_id', + 'mapped_by': self._to_camel_case_first_lower(buml_class.name), + 'source_class': buml_class.name, + 'join_table_name': f"{self._to_snake_case(buml_class.name)}_{self._to_snake_case(target_class)}", + 'source_join_column': f"{self._to_snake_case(buml_class.name)}_id", + 'target_join_column': f"{self._to_snake_case(target_class)}_id", + 'visibility': visibility, + 'is_owning_side': is_owning_side, + 'is_navigable': is_navigable, + 'cascade_types': cascade_types, + 'cascade_types_str': cascade_types_str, + 'fetch_type': fetch_type, + 'is_optional': is_optional, + 'side_description': side_description, + 'description': description + } + + relations.append(relation_data) + + return relations + + def _prepare_unique_constraints(self, buml_class: Class) -> List[Dict]: + """Pripremi podatke za unique constraints""" + constraints = [] + + if not buml_class.attributes: + return constraints + + unique_attrs = [] + for attr in buml_class.attributes: + if self._is_association_end(attr): + continue + + if 'email' in attr.name.lower() or 'username' in attr.name.lower(): + unique_attrs.append(attr.name) + + if unique_attrs: + constraint = { + 'name': f"uk_{self._to_snake_case(buml_class.name)}_{'_'.join([self._to_snake_case(a) for a in unique_attrs])}", + 'columns': ', '.join([f'"{self._to_snake_case(a)}"' for a in unique_attrs]) + } + constraints.append(constraint) + + return constraints + + def _generate_constructor_params(self, buml_class: Class) -> str: + """Generiši parametre za konstruktor""" + all_attrs = self._get_all_attributes(buml_class) + + if not all_attrs: + return "" + + params = [] + for attr in all_attrs: + java_type = self._map_type_to_java(attr.type) + attr_name = attr.name if hasattr(attr, 'name') else "unknown" + params.append(f"{java_type} {attr_name}") + + return ", ".join(params) + + def _generate_constructor_params_list(self, buml_class: Class) -> List[Dict]: + """Generiši listu parametara sa tipom""" + all_attrs = self._get_all_attributes(buml_class) + params = [] + + for attr in all_attrs: + java_type = self._map_type_to_java(attr.type) + attr_name = attr.name if hasattr(attr, 'name') else "unknown" + params.append({ + 'name': attr_name, + 'type': java_type + }) + + return params + + def _generate_constructor_assignments(self, buml_class: Class) -> List[str]: + """Generiši dodele vrednosti u konstruktoru""" + all_attrs = self._get_all_attributes(buml_class) + assignments = [] + + for attr in all_attrs: + attr_name = attr.name if hasattr(attr, 'name') else "unknown" + assignments.append(f"this.{attr_name} = {attr_name};") + + return assignments + + def _get_all_attributes(self, buml_class: Class) -> List: + """Preuzmi sve atribute (i obične i relacijske)""" + all_attrs = [] + + if buml_class.attributes: + for attr in buml_class.attributes: + if not self._is_association_end(attr): + all_attrs.append(attr) + + if buml_class.name in self.associations_by_class: + for assoc in self.associations_by_class[buml_class.name]: + for end in assoc.ends: + if hasattr(end, 'type') and end.type != buml_class: + all_attrs.append(end) + break + + return all_attrs + + def _prepare_getters_setters(self, buml_class: Class) -> List[Dict]: + """Pripremi podatke za getter i setter metode""" + getters_setters = [] + + if not buml_class.attributes: + return getters_setters + + for attr in buml_class.attributes: + if self._is_association_end(attr): + continue + + java_type = self._map_type_to_java(attr.type) + attr_name = attr.name if hasattr(attr, 'name') else "unknown" + method_name = self._capitalize_first(attr_name) + visibility = self._get_visibility(attr) + + getters_setters.append({ + 'java_type': java_type, + 'attribute_name': attr_name, + 'method_name': method_name, + 'visibility': visibility + }) + + return getters_setters + + def _is_association_end(self, attr: Property) -> bool: + """Proverite da li je atribut asocijacijski kraj""" + if hasattr(attr, 'type') and attr.type: + type_name = attr.type.name if hasattr(attr.type, 'name') else str(attr.type) + return type_name[0].isupper() and type_name not in self.JAVA_TYPE_MAPPING + return False + + def _map_type_to_java(self, buml_type) -> str: + """Mapira B-UML tip na Java tip""" + if buml_type is None: + return "Object" + + type_name = buml_type.name if hasattr(buml_type, 'name') else str(buml_type) + + if type_name in self.enumerations: + return type_name + + return self.JAVA_TYPE_MAPPING.get(type_name, type_name) + + def _determine_relation_type(self, source_end, target_end) -> str: + """Odredi tip relacije na osnovu multipliciteta""" + source_mult = self._get_multiplicity(source_end) + target_mult = self._get_multiplicity(target_end) + + if target_mult in ["0..1", "1"]: + if source_mult in ["0..*", "*"]: + return "ManyToOne" + return "OneToOne" + elif target_mult in ["0..*", "*"]: + if source_mult in ["0..*", "*"]: + return "ManyToMany" + return "OneToMany" + + return "ManyToOne" + + def _get_multiplicity(self, end) -> str: + """Pronađi multipliciteta asocijacionog kraja""" + if hasattr(end, 'multiplicity') and end.multiplicity: + mult = end.multiplicity + if hasattr(mult, 'lower') and hasattr(mult, 'upper'): + if mult.upper == -1: + return f"{mult.lower}..*" + return f"{mult.lower}..{mult.upper}" + return "0..1" + + @staticmethod + def _to_snake_case(name: str) -> str: + """Konvertuje CamelCase u snake_case""" + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + @staticmethod + def _to_camel_case_first_lower(name: str) -> str: + """Konvertuje CamelCase u camelCase""" + if not name: + return name + return name[0].lower() + name[1:] + + @staticmethod + def _capitalize_first(name: str) -> str: + """Velikim baš prvi karakter""" + if not name: + return name + return name[0].upper() + name[1:] + + @staticmethod + def _pluralize(name: str) -> str: + """Pluralizuje ime""" + if name.endswith('y'): + return name[:-1] + 'ies' + elif name.endswith(('s', 'x', 'z', 'ch', 'sh')): + return name + 'es' + else: + return name + 's' \ No newline at end of file diff --git a/besser/generators/spring/templates/__init__.py b/besser/generators/spring/templates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/besser/generators/spring/templates/entity.j2 b/besser/generators/spring/templates/entity.j2 new file mode 100644 index 000000000..963a245b5 --- /dev/null +++ b/besser/generators/spring/templates/entity.j2 @@ -0,0 +1,15 @@ +package {{ package_name }}; + +{% for import in imports -%} +import {{ import }}; +{% endfor -%} + +{% if is_abstract %} +@MappedSuperclass +{% else %} +@Entity +@Table(name="{{ table_name }}") +{% endif -%} +public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent %} extends {{parent}}{% endif %} { + +} \ No newline at end of file diff --git a/besser/generators/spring/templates/entity.java.j2 b/besser/generators/spring/templates/entity.java.j2 new file mode 100644 index 000000000..bd5b8634d --- /dev/null +++ b/besser/generators/spring/templates/entity.java.j2 @@ -0,0 +1,61 @@ +package {{ package_name }}; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.util.*; + +@Entity +@Table(name = "{{ class.name|lower }}") +public class {{ class.name }} { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + +{% for attr in attributes %} + +{% if not attr.optional %} + @NotNull +{% endif %} + @Column(name = "{{ attr.column }}") + {{ attr.visibility }} {{ attr.type }} {{ attr.name }}; + +{% endfor %} + +{% for attr in enum_attributes %} + + @Enumerated(EnumType.STRING) + {{ attr.visibility }} {{ attr.type }} {{ attr.name }}; + +{% endfor %} + +{% for rel in relations %} + +{{ rel.annotation }} + {{ rel.visibility }} {{ rel.type }} {{ rel.name }}{% if rel.collection %} = new ArrayList<>(){% endif %}; + +{% endfor %} + + public {{ class.name }}() {} + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + +{% for field in fields %} + + public {{ field.type }} get{{ field.name|capitalize }}() { + return {{ field.name }}; + } + + public void set{{ field.name|capitalize }}({{ field.type }} {{ field.name }}) { + this.{{ field.name }} = {{ field.name }}; + } + +{% endfor %} + +} \ No newline at end of file diff --git a/besser/generators/spring/templates/entity_template.j2 b/besser/generators/spring/templates/entity_template.j2 new file mode 100644 index 000000000..908e414c2 --- /dev/null +++ b/besser/generators/spring/templates/entity_template.j2 @@ -0,0 +1,314 @@ +package {{ package_name }}; + +{% for import in imports %} +import {{ import }}; +{% endfor %} + +/** + * Entity klasa {{ class_name }} + * Generisano automatski od BESSER B-UML modela + * + * BESSER dokumentacija: + * https://besser.readthedocs.io/en/latest/api/BUML/metamodel/api_structural.html + * + * Baeldung JPA/Hibernate asocijacije: + * https://www.baeldung.com/jpa-hibernate-associations + * +{% if description %} + * Opis: {{ description }} +{% endif %} + * @author Spring Entity Generator + * @version 1.0 + */ +{% if is_abstract %} +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +{% else %} +@Entity +{% endif %} +@Table(name = "{{ table_name }}"{% if unique_constraints %}, uniqueConstraints = { +{% for constraint in unique_constraints %} + @UniqueConstraint(name = "{{ constraint.name }}", columnNames = {{{ constraint.columns }}}){% if not loop.last %},{% endif %} +{% endfor %} +}{% endif %}) +public {% if is_abstract %}abstract {% endif %}class {{ class_name }}{% if parent %} extends {{ parent }}{% endif %} { + +{% for attribute in attributes %} +{% if attribute.is_id %} + /** + * Primarna ključna vrednost - jedinstveni identifikator entiteta + * Automatski se generiše pri kreiranju novog entiteta + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "{{ attribute.column_name }}") + {{ attribute.visibility }} {{ attribute.java_type }} {{ attribute.name }}; + +{% elif attribute.is_enum %} + /** + * Enumeracija - ograničeni skup mogućih vrednosti + * Mogućih vrednosti: {{ attribute.enum_values }} +{% if attribute.description %} * Opis: {{ attribute.description }} +{% endif %} +{% if attribute.is_readonly %} * Readonly: Da +{% endif %} +{% if attribute.is_derived %} * Izvedena vrednost: Da +{% endif %} + */ + @Column(name = "{{ attribute.column_name }}"{% if not attribute.nullable %}, nullable = false{% endif %}{% if attribute.length %}, length = {{ attribute.length }}{% endif %}) + @Enumerated(EnumType.STRING) + {{ attribute.visibility }} {{ attribute.java_type }} {{ attribute.name }}{% if attribute.default_value %} = {{ attribute.default_value }}{% endif %}; + +{% else %} + /** + * Атрибут: {{ attribute.name }} + * Tip: {{ attribute.java_type }} + * Obavezno: {% if attribute.nullable %}Ne{% else %}Da{% endif %} +{% if attribute.unique %} * Jedinstveno: Da +{% endif %} +{% if attribute.is_readonly %} * Readonly: Da +{% endif %} +{% if attribute.is_derived %} * Izvedena vrednost: Da +{% endif %} +{% if attribute.description %} * Opis: {{ attribute.description }} +{% endif %} + */ + @Column( + name = "{{ attribute.column_name }}" + {%- if not attribute.nullable %}, + nullable = false + {%- endif %} + {%- if attribute.unique %}, + unique = true + {%- endif %} + {%- if attribute.length %}, + length = {{ attribute.length }} + {%- endif %} + {%- if attribute.updatable != None %}, + updatable = {{ attribute.updatable|lower }} + {%- endif %} + {%- if attribute.insertable != None %}, + insertable = {{ attribute.insertable|lower }} + {%- endif %} + ) +{% if attribute.is_readonly %} + @org.hibernate.annotations.Immutable +{% endif %} + {{ attribute.visibility }} {{ attribute.java_type }} {{ attribute.name }}; + +{% endif %} +{% endfor %} + +{% for relation in relations %} +{% if relation.type == 'OneToOne' %} + /** + * OneToOne relacija - {{ relation.side_description }} + * Povezuje sa: {{ relation.target_class }} + * Fetch strategija: {{ relation.fetch_type }} + * Cascade akcije: {{ relation.cascade_types_str }} + * Navigable: {{ relation.is_navigable }} +{% if relation.description %} * Opis: {{ relation.description }} +{% endif %} + */ + @OneToOne( + fetch = FetchType.{{ relation.fetch_type }}, + cascade = {{% for cascade in relation.cascade_types %}CascadeType.{{ cascade }}{% if not loop.last %}, {% endif %}{% endfor %}} + {%- if relation.is_optional %}, + optional = true + {%- else %}, + optional = false + {%- endif %} + ) +{% if relation.is_owning_side %} + @JoinColumn( + name = "{{ relation.join_column_name }}", + referencedColumnName = "id", + nullable = {{ relation.is_optional|lower }}, + unique = true + ) +{% endif %} + {{ relation.visibility }} {{ relation.target_class }} {{ relation.field_name }}; + +{% elif relation.type == 'OneToMany' %} + /** + * OneToMany relacija - INVERSE SIDE + * Povezuje sa: {{ relation.target_class }} + * Broj stavki: Više (0..*) + * Fetch strategija: {{ relation.fetch_type }} + * Cascade akcije: {{ relation.cascade_types_str }} + * Navigable: {{ relation.is_navigable }} + * Mapped by: {{ relation.mapped_by }} +{% if relation.description %} * Opis: {{ relation.description }} +{% endif %} + */ + @OneToMany( + fetch = FetchType.{{ relation.fetch_type }}, + mappedBy = "{{ relation.mapped_by }}", + cascade = {{% for cascade in relation.cascade_types %}CascadeType.{{ cascade }}{% if not loop.last %}, {% endif %}{% endfor %}} + ) + {{ relation.visibility }} Set<{{ relation.target_class }}> {{ relation.plural_name }}; + +{% elif relation.type == 'ManyToOne' %} + /** + * ManyToOne relacija - OWNING SIDE + * Povezuje sa: {{ relation.target_class }} + * Fetch strategija: {{ relation.fetch_type }} + * Cascade akcije: {{ relation.cascade_types_str }} + * Navigable: {{ relation.is_navigable }} +{% if relation.description %} * Opis: {{ relation.description }} +{% endif %} + */ + @ManyToOne( + fetch = FetchType.{{ relation.fetch_type }}, + cascade = {{% for cascade in relation.cascade_types %}CascadeType.{{ cascade }}{% if not loop.last %}, {% endif %}{% endfor %}} + ) + @JoinColumn( + name = "{{ relation.join_column_name }}", + referencedColumnName = "id", + nullable = {{ relation.is_optional|lower }} + ) + {{ relation.visibility }} {{ relation.target_class }} {{ relation.field_name }}; + +{% elif relation.type == 'ManyToMany' %} + /** + * ManyToMany relacija - {% if relation.is_owning_side %}OWNING SIDE{% else %}INVERSE SIDE{% endif %} + * Povezuje sa: {{ relation.target_class }} + * Fetch strategija: {{ relation.fetch_type }} + * Cascade akcije: {{ relation.cascade_types_str }} + * Navigable: {{ relation.is_navigable }} +{% if not relation.is_owning_side %} * Mapped by: {{ relation.mapped_by }} +{% endif %} +{% if relation.description %} * Opis: {{ relation.description }} +{% endif %} + */ + @ManyToMany( + fetch = FetchType.{{ relation.fetch_type }}, + cascade = {{% for cascade in relation.cascade_types %}CascadeType.{{ cascade }}{% if not loop.last %}, {% endif %}{% endfor %}} + ) +{% if relation.is_owning_side %} + @JoinTable( + name = "{{ relation.join_table_name }}", + joinColumns = { + @JoinColumn(name = "{{ relation.source_join_column }}", referencedColumnName = "id") + }, + inverseJoinColumns = { + @JoinColumn(name = "{{ relation.target_join_column }}", referencedColumnName = "id") + } + ) +{% endif %} + {{ relation.visibility }} Set<{{ relation.target_class }}> {{ relation.plural_name }}; + +{% endif %} +{% endfor %} + + // ===================== KONSTRUKTORI ===================== + + /** + * Prazan konstruktor - obavezan za JPA/Hibernate + */ + public {{ class_name }}() { +{% for relation in relations %} +{% if relation.type in ['OneToMany', 'ManyToMany'] %} + this.{{ relation.plural_name }} = new HashSet<>(); +{% endif %} +{% endfor %} + } + +{% if constructor_params %} + /** + * Konstruktor sa svim atributima + * +{% for param in constructor_params_list %} + * @param {{ param.name }} {{ param.type }} +{% endfor %} + */ + public {{ class_name }}({{ constructor_params }}) { +{% for assignment in constructor_assignments %} + {{ assignment }} +{% endfor %} +{% for relation in relations %} +{% if relation.type in ['OneToMany', 'ManyToMany'] %} + this.{{ relation.plural_name }} = new HashSet<>(); +{% endif %} +{% endfor %} + } + +{% endif %} + + // ===================== GETTERI I SETTERI ===================== + +{% for getter_setter in getters_setters %} + /** + * Preuzmi vrednost {{ getter_setter.attribute_name }} + * + * @return {{ getter_setter.java_type }} + */ + {{ getter_setter.visibility }} {{ getter_setter.java_type }} get{{ getter_setter.method_name }}() { + return {{ getter_setter.attribute_name }}; + } + + /** + * Postavi vrednost {{ getter_setter.attribute_name }} + * + * @param {{ getter_setter.attribute_name }} {{ getter_setter.java_type }} + */ + {{ getter_setter.visibility }} void set{{ getter_setter.method_name }}({{ getter_setter.java_type }} {{ getter_setter.attribute_name }}) { + this.{{ getter_setter.attribute_name }} = {{ getter_setter.attribute_name }}; + } + +{% endfor %} + + // ===================== UTILITY METODE ===================== + + /** + * Vraća string reprezentaciju entiteta + * + * @return String reprezentacija + */ + @Override + public String toString() { + return "{{ class_name }}{" + + "id=" + id + +{% for attribute in attributes %} +{% if not attribute.is_id %} + ", {{ attribute.name }}='" + {{ attribute.name }} + '\'' + +{% endif %} +{% endfor %} + '}'; + } + + /** + * Poredi dva entiteta po ID-u + * Koristi ID jer je to jedinstven identifikator + * + * @param o Object za poređenje + * @return true ako su entiteti jednaki + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + {{ class_name }} that = ({{ class_name }}) o; + + // Koristi ID za poređenje jer je to jedinstven identifikator + if (id == null || that.id == null) { + return false; + } + return id.equals(that.id); + } + + /** + * Generiše hash kod entiteta + * Koristi ID jer je to jedinstven identifikator + * + * @return hash kod + */ + @Override + public int hashCode() { + return id == null ? 0 : id.hashCode(); + } +} \ No newline at end of file diff --git a/besser/generators/spring/templates/enum.java.j2 b/besser/generators/spring/templates/enum.java.j2 new file mode 100644 index 000000000..cba70cb3e --- /dev/null +++ b/besser/generators/spring/templates/enum.java.j2 @@ -0,0 +1,9 @@ +package {{ package_name }}; + +public enum {{ enum.name }} { + +{% for lit in enum.literals %} + {{ lit.name }}{% if not loop.last %},{% endif %} +{% endfor %} + +} \ No newline at end of file From 657dd00305bc670c1f22d0cfef12e6ef9adf94ec Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Sun, 8 Mar 2026 22:15:00 +0100 Subject: [PATCH 02/16] attrs, enums, constructors, getters, setters --- .../generators/spring/generated/Computer.java | 19 ++++ besser/generators/spring/generated/Hand.java | 5 ++ besser/generators/spring/generated/Role.java | 5 ++ besser/generators/spring/generated/Thing.java | 4 +- besser/generators/spring/generated/User.java | 83 +++++++++++++++++ besser/generators/spring/generator.py | 88 +++++++++++++++++-- besser/generators/spring/proba.py | 17 ++-- besser/generators/spring/templates/entity.j2 | 34 +++++++ .../generators/spring/templates/enum.java.j2 | 8 +- 9 files changed, 243 insertions(+), 20 deletions(-) create mode 100644 besser/generators/spring/generated/Hand.java create mode 100644 besser/generators/spring/generated/Role.java diff --git a/besser/generators/spring/generated/Computer.java b/besser/generators/spring/generated/Computer.java index 806781d58..2c23c8562 100644 --- a/besser/generators/spring/generated/Computer.java +++ b/besser/generators/spring/generated/Computer.java @@ -3,9 +3,28 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import jakarta.persistence.Id; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Column; @Entity @Table(name="computers") public class Computer extends Thing { + + @Column(name = "name", nullable = false) + public String name; + + public Computer() { } + public Computer(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + } \ No newline at end of file diff --git a/besser/generators/spring/generated/Hand.java b/besser/generators/spring/generated/Hand.java new file mode 100644 index 000000000..03b9752b8 --- /dev/null +++ b/besser/generators/spring/generated/Hand.java @@ -0,0 +1,5 @@ +package spring.entities; + +public enum Hand { + LEFT, RIGHT +} \ No newline at end of file diff --git a/besser/generators/spring/generated/Role.java b/besser/generators/spring/generated/Role.java new file mode 100644 index 000000000..592989292 --- /dev/null +++ b/besser/generators/spring/generated/Role.java @@ -0,0 +1,5 @@ +package spring.entities; + +public enum Role { + ADMIN, USER +} \ No newline at end of file diff --git a/besser/generators/spring/generated/Thing.java b/besser/generators/spring/generated/Thing.java index 053376e53..2347ec99a 100644 --- a/besser/generators/spring/generated/Thing.java +++ b/besser/generators/spring/generated/Thing.java @@ -3,9 +3,11 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import jakarta.persistence.Id; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Column; import javax.persistence.MappedSuperclass; @MappedSuperclass public abstract class Thing { - + } \ No newline at end of file diff --git a/besser/generators/spring/generated/User.java b/besser/generators/spring/generated/User.java index a38c620fb..fb27b3ae4 100644 --- a/besser/generators/spring/generated/User.java +++ b/besser/generators/spring/generated/User.java @@ -3,9 +3,92 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import jakarta.persistence.Id; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Column; +import jakarta.persistence.Enumerated; +import java.time.Duration; @Entity @Table(name="users") public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "email") + public String email; + + @Enumerated(EnumType.STRING) + public Hand hand; + + @Enumerated(EnumType.STRING) + private Role role; + + @Column(name = "age", nullable = false) + public Integer age = 2; + + @Column(name = "birthday", nullable = false) + protected Duration birthday; + + @Column(name = "first_name") + public String firstName = "John"; + + public User() { } + public User(String email, Hand hand, Role role, Integer age, Duration birthday, String firstName) { + this.email = email; + this.hand = hand; + this.role = role; + this.age = age; + this.birthday = birthday; + this.firstName = firstName; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Hand getHand() { + return this.hand; + } + + public void setHand(Hand hand) { + this.hand = hand; + } + + public Role getRole() { + return this.role; + } + + public void setRole(Role role) { + this.role = role; + } + + public Integer getAge() { + return this.age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Duration getBirthday() { + return this.birthday; + } + + public void setBirthday(Duration birthday) { + this.birthday = birthday; + } + + public String getFirstname() { + return this.firstName; + } + + public void setFirstname(String firstName) { + this.firstName = firstName; + } + } \ No newline at end of file diff --git a/besser/generators/spring/generator.py b/besser/generators/spring/generator.py index 32ad275d9..f5ca1ad3f 100644 --- a/besser/generators/spring/generator.py +++ b/besser/generators/spring/generator.py @@ -1,27 +1,40 @@ import os +import re from typing import List from jinja2 import Environment, FileSystemLoader from besser.BUML.metamodel.structural import DomainModel -from besser.BUML.metamodel.structural.structural import Class +from besser.BUML.metamodel.structural.structural import Class, Enumeration, StringType, BooleanType, DateTimeType, DateType, FloatType, IntegerType, TimeDeltaType, TimeType from besser.generators import GeneratorInterface -from besser.utilities.utils import sort_by_timestamp - class SpringEntityGenerator(GeneratorInterface): + JAVA_TYPES = { + StringType.name: "String", + BooleanType.name: "Boolean", + IntegerType.name: "Integer", + FloatType.name: "Float", + DateType.name: "LocalDate", + DateTimeType.name: "LocalDateTime", + TimeType.name: "LocalDateTime", + TimeDeltaType.name: "Duration" + } + def __init__(self, model: DomainModel, output_dir: str = "./generated/entities", package_name: str = "com.example.entities"): super().__init__(model, output_dir) self.package_name: str = package_name + self.enumerations: set[Enumeration] = model.get_enumerations() def generate(self): model: DomainModel = self.model + + for enum in model.get_enumerations(): + self._generate_enum_file(enum) for cls in model.classes_sorted_by_inheritance(): self._generate_class_file(cls) - def _generate_class_file(self, cls: Class): file_path = self.build_generation_path(file_name=f"{cls.name.capitalize()}.java") @@ -29,10 +42,23 @@ def _generate_class_file(self, cls: Class): env = Environment(loader=FileSystemLoader(templates_path)) entity_template = env.get_template("entity.j2") - imports: List[str] = self._get_default_imports() + imports: List[str] = self._get_default_imports() + self._get_specific_imports_for_class(cls) - if cls.is_abstract: - imports.append("javax.persistence.MappedSuperclass") + attributes: List[object] = [] + + for attr in cls.attributes: + is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) + + attributes.append({ + "is_id": attr.is_id, + "column_name": self._to_snake_case(attr.name), + "is_optional": attr.is_optional, + "is_enum": is_enum, + "visibility": attr.visibility, + "type": self.JAVA_TYPES[attr.type.name] if not is_enum else attr.type.name, + "name": attr.name, + "default_value": f"\"{attr.default_value}\"" if attr.default_value and attr.type.name == "str" else attr.default_value + }) context = { "cls": cls, @@ -40,20 +66,61 @@ def _generate_class_file(self, cls: Class): "imports": imports, "is_abstract": cls.is_abstract, "table_name": self._pluralize(cls.name.lower()), - "parent": cls.parents().pop().name if cls.parents() else None + "parent": cls.parents().pop().name if cls.parents() else None, + "attributes": sorted( + attributes, + key=lambda a: (not a["is_id"], not a["is_enum"], a["name"]) + ) } with open(file_path, mode="w", encoding="utf-8") as f: generated_code = entity_template.render(**context) f.write(generated_code) + def _generate_enum_file(self, enum: Enumeration): + file_path = self.build_generation_path(file_name=f"{enum.name.capitalize()}.java") + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path)) + entity_template = env.get_template("enum.java.j2") + + context = { + "package_name": self.package_name, + "name": enum.name.capitalize(), + "literals": enum.literals + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = entity_template.render(**context) + f.write(generated_code) + def _get_default_imports(self) -> List[str]: return [ "jakarta.persistence.Entity", "jakarta.persistence.Table", "jakarta.persistence.Id", + "jakarta.persistence.GeneratedValue", + "jakarta.persistence.Column", ] + def _get_specific_imports_for_class(self, cls: Class) -> List[str]: + imports: List[str] = [] + + if cls.is_abstract: + imports.append("javax.persistence.MappedSuperclass") + + if any(any(attr.type.name == enum.name for enum in self.enumerations) for attr in cls.attributes): + imports.append("jakarta.persistence.Enumerated") + + for attr in cls.attributes: + if attr.type.name == DateType.name: + imports.append("java.time.LocalDate") + elif attr.type.name in [DateTimeType.name, TimeType.name]: + imports.append("java.time.LocalDateTime") + elif attr.type.name == TimeDeltaType.name: + imports.append("java.time.Duration") + + return imports + @staticmethod def _pluralize(name: str) -> str: if name.endswith("y"): @@ -62,3 +129,8 @@ def _pluralize(name: str) -> str: return name + "es" else: return name + "s" + + @staticmethod + def _to_snake_case(name: str) -> str: + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index e973699c6..ba2a1c6aa 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -14,7 +14,7 @@ # Uvezi sa eksplicitnom putanjom from besser.BUML.metamodel.structural.structural import ( - DomainModel, Class, Generalization, Property, Type, Association, Enumeration, EnumerationLiteral + DateTimeType, DomainModel, Class, Generalization, IntegerType, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral ) from besser.generators.spring.generator import SpringEntityGenerator @@ -23,19 +23,26 @@ def proba_jednostavna_klasa(): current_dir = Path(__file__).parent output_dir = current_dir / "generated" output_dir.mkdir(exist_ok=True) + + role = Enumeration(name="Role", literals={EnumerationLiteral(name="USER"), EnumerationLiteral(name="ADMIN")}) + hand = Enumeration(name="Hand", literals={EnumerationLiteral(name="LEFT"), EnumerationLiteral(name="RIGHT")}) user = Class(name="User") - user.add_attribute(Property(name="email", type=Type("String"))) - user.add_attribute(Property(name="firstName", type=Type("String"))) + user.add_attribute(Property(name="email", type=StringType, is_id=True, is_optional=True)) + user.add_attribute(Property(name="firstName", type=StringType, is_optional=True, default_value="John")) + user.add_attribute(Property(name="age", type=IntegerType, default_value=2)) + user.add_attribute(Property(name="role", type=role, visibility="private")) + user.add_attribute(Property(name="birthday", type=TimeDeltaType, visibility="protected")) + user.add_attribute(Property(name="hand", type=hand)) thing = Class(name="Thing", is_abstract=True) computer = Class(name="Computer") - computer.add_attribute(Property(name="name", type=Type("String"))) + computer.add_attribute(Property(name="name", type=StringType)) gen1 = Generalization(general=thing, specific=computer) - model = DomainModel(name="proba", types={user, thing, computer}, generalizations={gen1}) + model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}) generator = SpringEntityGenerator( model, diff --git a/besser/generators/spring/templates/entity.j2 b/besser/generators/spring/templates/entity.j2 index 963a245b5..60e6cf3a9 100644 --- a/besser/generators/spring/templates/entity.j2 +++ b/besser/generators/spring/templates/entity.j2 @@ -11,5 +11,39 @@ import {{ import }}; @Table(name="{{ table_name }}") {% endif -%} public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent %} extends {{parent}}{% endif %} { + {% for attribute in attributes -%} + {% if attribute.is_id %} + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + {%- endif %} + {% if attribute.is_enum -%} + @Enumerated(EnumType.STRING) + {% else -%} + @Column(name = "{{ attribute.column_name }}"{% if not attribute.is_optional %}, nullable = false{% endif %}) + {% endif %} + {{- attribute.visibility }} {{ attribute.type }} {{ attribute.name }}{% if attribute.default_value %} = {{ attribute.default_value }}{% endif %}; + {% endfor -%} + {% if not is_abstract %} + public {{ cls.name }}() { } + + public {{ cls.name }}( + {%- for attribute in attributes -%} + {{ attribute.type }} {{ attribute.name }}{% if not loop.last %}, {% endif %} + {%- endfor %}) { + {%- for attribute in attributes %} + this.{{ attribute.name }} = {{ attribute.name }}; + {%- endfor %} + } + {% endif -%} + + {% for attribute in attributes %} + public {{ attribute.type }} get{{ attribute.name | capitalize }}() { + return this.{{ attribute.name }}; + } + + public void set{{ attribute.name | capitalize }}({{ attribute.type }} {{ attribute.name }}) { + this.{{ attribute.name }} = {{ attribute.name }}; + } + {% endfor %} } \ No newline at end of file diff --git a/besser/generators/spring/templates/enum.java.j2 b/besser/generators/spring/templates/enum.java.j2 index cba70cb3e..7d217911a 100644 --- a/besser/generators/spring/templates/enum.java.j2 +++ b/besser/generators/spring/templates/enum.java.j2 @@ -1,9 +1,5 @@ package {{ package_name }}; -public enum {{ enum.name }} { - -{% for lit in enum.literals %} - {{ lit.name }}{% if not loop.last %},{% endif %} -{% endfor %} - +public enum {{ name }} { + {% for lit in literals %}{{ lit.name }}{% if not loop.last %}, {% endif %}{% endfor %} } \ No newline at end of file From e96b6475be939eda28c9c15f5d3c6f1740181e53 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Mon, 9 Mar 2026 17:03:16 +0100 Subject: [PATCH 03/16] list --- .../generators/spring/generated/Computer.java | 11 ++-- besser/generators/spring/generated/Role.java | 2 +- besser/generators/spring/generated/Thing.java | 11 ++-- besser/generators/spring/generated/User.java | 43 ++++++-------- besser/generators/spring/generator.py | 56 ++++++++++++------- besser/generators/spring/proba.py | 14 ++--- besser/generators/spring/templates/entity.j2 | 6 +- 7 files changed, 75 insertions(+), 68 deletions(-) diff --git a/besser/generators/spring/generated/Computer.java b/besser/generators/spring/generated/Computer.java index 2c23c8562..e892e5622 100644 --- a/besser/generators/spring/generated/Computer.java +++ b/besser/generators/spring/generated/Computer.java @@ -1,10 +1,11 @@ package spring.entities; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.persistence.Id; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Column; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; @Entity @Table(name="computers") diff --git a/besser/generators/spring/generated/Role.java b/besser/generators/spring/generated/Role.java index 592989292..df68f847d 100644 --- a/besser/generators/spring/generated/Role.java +++ b/besser/generators/spring/generated/Role.java @@ -1,5 +1,5 @@ package spring.entities; public enum Role { - ADMIN, USER + USER, ADMIN } \ No newline at end of file diff --git a/besser/generators/spring/generated/Thing.java b/besser/generators/spring/generated/Thing.java index 2347ec99a..facdefe06 100644 --- a/besser/generators/spring/generated/Thing.java +++ b/besser/generators/spring/generated/Thing.java @@ -1,11 +1,12 @@ package spring.entities; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.persistence.Id; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Column; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; import javax.persistence.MappedSuperclass; +import javax.persistence.Table; @MappedSuperclass public abstract class Thing { diff --git a/besser/generators/spring/generated/User.java b/besser/generators/spring/generated/User.java index fb27b3ae4..fb02eaf1b 100644 --- a/besser/generators/spring/generated/User.java +++ b/besser/generators/spring/generated/User.java @@ -1,12 +1,15 @@ package spring.entities; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.persistence.Id; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Column; -import jakarta.persistence.Enumerated; import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; @Entity @Table(name="users") @@ -17,11 +20,8 @@ public class User { @Column(name = "email") public String email; - @Enumerated(EnumType.STRING) - public Hand hand; - - @Enumerated(EnumType.STRING) - private Role role; + @Column(name = "nicknames", nullable = false) + public List nicknames = new ArrayList<>(Arrays.asList(1.2f, 3.2f)); @Column(name = "age", nullable = false) public Integer age = 2; @@ -34,10 +34,9 @@ public class User { public User() { } - public User(String email, Hand hand, Role role, Integer age, Duration birthday, String firstName) { + public User(String email, List nicknames, Integer age, Duration birthday, String firstName) { this.email = email; - this.hand = hand; - this.role = role; + this.nicknames = nicknames; this.age = age; this.birthday = birthday; this.firstName = firstName; @@ -51,20 +50,12 @@ public void setEmail(String email) { this.email = email; } - public Hand getHand() { - return this.hand; - } - - public void setHand(Hand hand) { - this.hand = hand; - } - - public Role getRole() { - return this.role; + public List getNicknames() { + return this.nicknames; } - public void setRole(Role role) { - this.role = role; + public void setNicknames(List nicknames) { + this.nicknames = nicknames; } public Integer getAge() { diff --git a/besser/generators/spring/generator.py b/besser/generators/spring/generator.py index f5ca1ad3f..a1a6fb402 100644 --- a/besser/generators/spring/generator.py +++ b/besser/generators/spring/generator.py @@ -42,34 +42,44 @@ def _generate_class_file(self, cls: Class): env = Environment(loader=FileSystemLoader(templates_path)) entity_template = env.get_template("entity.j2") - imports: List[str] = self._get_default_imports() + self._get_specific_imports_for_class(cls) + imports: set[str] = self._get_default_imports().union(self._get_specific_imports_for_class(cls)) attributes: List[object] = [] for attr in cls.attributes: is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) + is_list: bool = attr.multiplicity.max != 1 + attr_type = None + + if is_enum: + attr_type = attr.type.name + else: + attr_type = self.JAVA_TYPES[attr.type.name] + if is_list: + attr_type = f"List<{attr_type}>" attributes.append({ "is_id": attr.is_id, "column_name": self._to_snake_case(attr.name), "is_optional": attr.is_optional, "is_enum": is_enum, + "is_list": is_list, "visibility": attr.visibility, - "type": self.JAVA_TYPES[attr.type.name] if not is_enum else attr.type.name, + "type": attr_type, "name": attr.name, - "default_value": f"\"{attr.default_value}\"" if attr.default_value and attr.type.name == "str" else attr.default_value + "default_value": f"\"{attr.default_value}\"" if attr.default_value and attr.type.name == "str" and not is_list else attr.default_value }) context = { "cls": cls, "package_name": self.package_name, - "imports": imports, + "imports": sorted(list(imports)), "is_abstract": cls.is_abstract, "table_name": self._pluralize(cls.name.lower()), "parent": cls.parents().pop().name if cls.parents() else None, "attributes": sorted( attributes, - key=lambda a: (not a["is_id"], not a["is_enum"], a["name"]) + key=lambda a: (not a["is_id"], not a["is_enum"], not a["is_list"], a["name"]) ) } @@ -93,31 +103,37 @@ def _generate_enum_file(self, enum: Enumeration): generated_code = entity_template.render(**context) f.write(generated_code) - def _get_default_imports(self) -> List[str]: - return [ - "jakarta.persistence.Entity", - "jakarta.persistence.Table", - "jakarta.persistence.Id", - "jakarta.persistence.GeneratedValue", - "jakarta.persistence.Column", - ] + def _get_default_imports(self) -> set[str]: + return set([ + "javax.persistence.Entity", + "javax.persistence.Table", + "javax.persistence.Id", + "javax.persistence.GeneratedValue", + "javax.persistence.GenerationType", + "javax.persistence.Column", + ]) - def _get_specific_imports_for_class(self, cls: Class) -> List[str]: - imports: List[str] = [] + def _get_specific_imports_for_class(self, cls: Class) -> set[str]: + imports: set[str] = set() if cls.is_abstract: - imports.append("javax.persistence.MappedSuperclass") + imports.add("javax.persistence.MappedSuperclass") if any(any(attr.type.name == enum.name for enum in self.enumerations) for attr in cls.attributes): - imports.append("jakarta.persistence.Enumerated") + imports.add("javax.persistence.Enumerated") + imports.add("javax.persistence.EnumType") for attr in cls.attributes: if attr.type.name == DateType.name: - imports.append("java.time.LocalDate") + imports.add("java.time.LocalDate") elif attr.type.name in [DateTimeType.name, TimeType.name]: - imports.append("java.time.LocalDateTime") + imports.add("java.time.LocalDateTime") elif attr.type.name == TimeDeltaType.name: - imports.append("java.time.Duration") + imports.add("java.time.Duration") + elif attr.multiplicity.max != 1: + imports.add("java.util.List") + imports.add("java.util.ArrayList") + imports.add("java.util.Arrays") return imports diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index ba2a1c6aa..81cf6a455 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -5,16 +5,9 @@ import sys import os from pathlib import Path -import tempfile -import shutil -# Dodaj root direktorijum BESSER-a -# root_dir = Path(__file__).parent -# sys.path.insert(0, str(root_dir)) - -# Uvezi sa eksplicitnom putanjom from besser.BUML.metamodel.structural.structural import ( - DateTimeType, DomainModel, Class, Generalization, IntegerType, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral + DateTimeType, DomainModel, Class, FloatType, Generalization, IntegerType, Multiplicity, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral ) from besser.generators.spring.generator import SpringEntityGenerator @@ -31,9 +24,10 @@ def proba_jednostavna_klasa(): user.add_attribute(Property(name="email", type=StringType, is_id=True, is_optional=True)) user.add_attribute(Property(name="firstName", type=StringType, is_optional=True, default_value="John")) user.add_attribute(Property(name="age", type=IntegerType, default_value=2)) - user.add_attribute(Property(name="role", type=role, visibility="private")) + # user.add_attribute(Property(name="role", type=role, visibility="private")) user.add_attribute(Property(name="birthday", type=TimeDeltaType, visibility="protected")) - user.add_attribute(Property(name="hand", type=hand)) + # user.add_attribute(Property(name="hand", type=hand)) + user.add_attribute(Property(name="nicknames", type=FloatType, multiplicity=Multiplicity(0, "*"), default_value="1.2f, 3.2f")) thing = Class(name="Thing", is_abstract=True) diff --git a/besser/generators/spring/templates/entity.j2 b/besser/generators/spring/templates/entity.j2 index 60e6cf3a9..bd38e911a 100644 --- a/besser/generators/spring/templates/entity.j2 +++ b/besser/generators/spring/templates/entity.j2 @@ -20,8 +20,12 @@ public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent @Enumerated(EnumType.STRING) {% else -%} @Column(name = "{{ attribute.column_name }}"{% if not attribute.is_optional %}, nullable = false{% endif %}) - {% endif %} + {% endif -%} + {% if not attribute.is_list %} {{- attribute.visibility }} {{ attribute.type }} {{ attribute.name }}{% if attribute.default_value %} = {{ attribute.default_value }}{% endif %}; + {%- else -%} + {{- attribute.visibility }} {{ attribute.type }} {{ attribute.name }} = new ArrayList<>({% if attribute.default_value %}Arrays.asList({{ attribute.default_value }}){% endif %}); + {%- endif %} {% endfor -%} {% if not is_abstract %} From cddd32da2b6a72048f601141ceecaf10209073e6 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Wed, 11 Mar 2026 22:17:53 +0100 Subject: [PATCH 04/16] relations --- .../generators/spring/generated/Computer.java | 21 ++-- besser/generators/spring/generated/User.java | 52 +++------ besser/generators/spring/generator.py | 109 ++++++++++++++---- besser/generators/spring/proba.py | 34 ++++-- besser/generators/spring/templates/entity.j2 | 39 ++++++- 5 files changed, 177 insertions(+), 78 deletions(-) diff --git a/besser/generators/spring/generated/Computer.java b/besser/generators/spring/generated/Computer.java index e892e5622..ec4e312dd 100644 --- a/besser/generators/spring/generated/Computer.java +++ b/besser/generators/spring/generated/Computer.java @@ -8,24 +8,19 @@ import javax.persistence.Table; @Entity -@Table(name="computers") +@Table(name = "computers") public class Computer extends Thing { - @Column(name = "name", nullable = false) - public String name; + @OneToOne + @JoinColumn(name = "user_id") + private User owner; + + @ManyToOne + @JoinColumn(name = "user_id") + private User ownerr; public Computer() { } - public Computer(String name) { - this.name = name; - } - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } } \ No newline at end of file diff --git a/besser/generators/spring/generated/User.java b/besser/generators/spring/generated/User.java index fb02eaf1b..3098d8c16 100644 --- a/besser/generators/spring/generated/User.java +++ b/besser/generators/spring/generated/User.java @@ -1,18 +1,16 @@ package spring.entities; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity -@Table(name="users") +@Table(name = "users") public class User { @Id @@ -20,25 +18,21 @@ public class User { @Column(name = "email") public String email; - @Column(name = "nicknames", nullable = false) - public List nicknames = new ArrayList<>(Arrays.asList(1.2f, 3.2f)); - - @Column(name = "age", nullable = false) - public Integer age = 2; - - @Column(name = "birthday", nullable = false) - protected Duration birthday; + @Enumerated(EnumType.STRING) + private Role role; @Column(name = "first_name") public String firstName = "John"; + @OneToMany(mappedBy = "ownerr") + + private List computers = new ArrayList<>(); + public User() { } - public User(String email, List nicknames, Integer age, Duration birthday, String firstName) { + public User(String email, Role role, String firstName) { this.email = email; - this.nicknames = nicknames; - this.age = age; - this.birthday = birthday; + this.role = role; this.firstName = firstName; } @@ -50,28 +44,12 @@ public void setEmail(String email) { this.email = email; } - public List getNicknames() { - return this.nicknames; - } - - public void setNicknames(List nicknames) { - this.nicknames = nicknames; - } - - public Integer getAge() { - return this.age; - } - - public void setAge(Integer age) { - this.age = age; - } - - public Duration getBirthday() { - return this.birthday; + public Role getRole() { + return this.role; } - public void setBirthday(Duration birthday) { - this.birthday = birthday; + public void setRole(Role role) { + this.role = role; } public String getFirstname() { diff --git a/besser/generators/spring/generator.py b/besser/generators/spring/generator.py index a1a6fb402..42fc7ac21 100644 --- a/besser/generators/spring/generator.py +++ b/besser/generators/spring/generator.py @@ -1,9 +1,10 @@ +from collections import defaultdict import os import re from typing import List from jinja2 import Environment, FileSystemLoader from besser.BUML.metamodel.structural import DomainModel -from besser.BUML.metamodel.structural.structural import Class, Enumeration, StringType, BooleanType, DateTimeType, DateType, FloatType, IntegerType, TimeDeltaType, TimeType +from besser.BUML.metamodel.structural.structural import BinaryAssociation, Class, Enumeration, Property, StringType, BooleanType, DateTimeType, DateType, FloatType, IntegerType, TimeDeltaType, TimeType from besser.generators import GeneratorInterface class SpringEntityGenerator(GeneratorInterface): @@ -29,14 +30,20 @@ def __init__(self, model: DomainModel, def generate(self): model: DomainModel = self.model + assoc_map: defaultdict[str, List[BinaryAssociation]] = defaultdict(list) + + for assoc in model.associations: + end1, end2 = list(assoc.ends) + assoc_map[end1.type.name].append(assoc) + assoc_map[end2.type.name].append(assoc) for enum in model.get_enumerations(): self._generate_enum_file(enum) for cls in model.classes_sorted_by_inheritance(): - self._generate_class_file(cls) + self._generate_class_file(cls, assoc_map[cls.name]) - def _generate_class_file(self, cls: Class): + def _generate_class_file(self, cls: Class, assocs_for_class: List[BinaryAssociation]): file_path = self.build_generation_path(file_name=f"{cls.name.capitalize()}.java") templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") env = Environment(loader=FileSystemLoader(templates_path)) @@ -44,6 +51,25 @@ def _generate_class_file(self, cls: Class): imports: set[str] = self._get_default_imports().union(self._get_specific_imports_for_class(cls)) + context = { + "cls": cls, + "package_name": self.package_name, + "imports": sorted(list(imports)), + "is_abstract": cls.is_abstract, + "table_name": self._pluralize(cls.name.lower()), + "parent": cls.parents().pop().name if cls.parents() else None, + "attributes": sorted( + self._prepare_attributes(cls), + key=lambda a: (not a["is_id"], not a["is_enum"], not a["is_list"], a["name"]) + ), + "relations": self._prepare_relations(cls, assocs_for_class), + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = entity_template.render(**context) + f.write(generated_code) + + def _prepare_attributes(self, cls: Class) -> List[object]: attributes: List[object] = [] for attr in cls.attributes: @@ -69,23 +95,65 @@ def _generate_class_file(self, cls: Class): "name": attr.name, "default_value": f"\"{attr.default_value}\"" if attr.default_value and attr.type.name == "str" and not is_list else attr.default_value }) - - context = { - "cls": cls, - "package_name": self.package_name, - "imports": sorted(list(imports)), - "is_abstract": cls.is_abstract, - "table_name": self._pluralize(cls.name.lower()), - "parent": cls.parents().pop().name if cls.parents() else None, - "attributes": sorted( - attributes, - key=lambda a: (not a["is_id"], not a["is_enum"], not a["is_list"], a["name"]) - ) - } - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = entity_template.render(**context) - f.write(generated_code) + return attributes + + def _prepare_relations(self, cls: Class, assocs_for_class: List[BinaryAssociation]) -> List[object]: + relations: List[object] = [] + + for assoc in assocs_for_class: + ends: List[Property] = list(assoc.ends) + end1, end2 = ends + + if end1.type.name == cls.name and end2.is_navigable: + source: Property = end1 + target: Property = end2 + elif end2.type.name == cls.name and end1.is_navigable: + source: Property = end2 + target: Property = end1 + else: + continue + + bidirectional: bool = source.is_navigable and target.is_navigable + source_many = source.multiplicity.max != 1 + target_many = target.multiplicity.max != 1 + + relation: str = None + mapped_by: str = None + owning: bool = True + + if not source_many and not target_many: + relation = "OneToOne" + if bidirectional: + mapped_by = source.name + elif not source_many and target_many: + relation = "OneToMany" + if bidirectional: + owning = False + mapped_by = source.name + elif source_many and not target_many: + relation = "ManyToOne" + elif source_many and target_many: + relation = "ManyToMany" + if bidirectional: + mapped_by = source.name + + relations.append({ + "assoc": assoc.name, + "source_property": source.name, + "target_property": target.name, + "source_cls": source.type.name, + "target_cls": target.type.name, + "relation": relation, + "mapped_by": mapped_by, + "join_column": f"{self._to_snake_case(target.type.name)}_id", + "owning": owning, + "is_list": target_many, + "type": f"List<{target.type.name}>" if target_many else target.type.name, + "to_snake_case": self._to_snake_case + }) + + return relations def _generate_enum_file(self, enum: Enumeration): file_path = self.build_generation_path(file_name=f"{enum.name.capitalize()}.java") @@ -130,7 +198,8 @@ def _get_specific_imports_for_class(self, cls: Class) -> set[str]: imports.add("java.time.LocalDateTime") elif attr.type.name == TimeDeltaType.name: imports.add("java.time.Duration") - elif attr.multiplicity.max != 1: + + if attr.multiplicity.max != 1: imports.add("java.util.List") imports.add("java.util.ArrayList") imports.add("java.util.Arrays") diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index 81cf6a455..54855fc76 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -7,7 +7,7 @@ from pathlib import Path from besser.BUML.metamodel.structural.structural import ( - DateTimeType, DomainModel, Class, FloatType, Generalization, IntegerType, Multiplicity, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral + BinaryAssociation, DateTimeType, DomainModel, Class, FloatType, Generalization, IntegerType, Multiplicity, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral ) from besser.generators.spring.generator import SpringEntityGenerator @@ -23,20 +23,40 @@ def proba_jednostavna_klasa(): user = Class(name="User") user.add_attribute(Property(name="email", type=StringType, is_id=True, is_optional=True)) user.add_attribute(Property(name="firstName", type=StringType, is_optional=True, default_value="John")) - user.add_attribute(Property(name="age", type=IntegerType, default_value=2)) - # user.add_attribute(Property(name="role", type=role, visibility="private")) - user.add_attribute(Property(name="birthday", type=TimeDeltaType, visibility="protected")) + # user.add_attribute(Property(name="age", type=IntegerType, default_value=2)) + user.add_attribute(Property(name="role", type=role, visibility="private")) + # user.add_attribute(Property(name="birthday", type=TimeDeltaType, visibility="protected")) # user.add_attribute(Property(name="hand", type=hand)) - user.add_attribute(Property(name="nicknames", type=FloatType, multiplicity=Multiplicity(0, "*"), default_value="1.2f, 3.2f")) + # user.add_attribute(Property(name="nicknames", type=DateTimeType, multiplicity=Multiplicity(0, "*"), default_value="1.2f, 3.2f")) thing = Class(name="Thing", is_abstract=True) computer = Class(name="Computer") - computer.add_attribute(Property(name="name", type=StringType)) + # computer.add_attribute(Property(name="name", type=StringType)) gen1 = Generalization(general=thing, specific=computer) - model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}) + assoc = BinaryAssociation(name="onetoone", ends={ + Property(name="computer", type=computer, multiplicity=Multiplicity(1, 1), is_navigable=False), + Property(name="owner", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True) + }) + + assoc2 = BinaryAssociation(name="onetomany", ends={ + Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True), + Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True) + }) + + # assoc2 = BinaryAssociation(name="manytoone", ends={ + # Property(name="computerss", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), + # Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True) + # }) + + # assoc = BinaryAssociation(name="ManyToMany", ends={ + # Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), + # Property(name="owners", type=user, multiplicity=Multiplicity(0, "*"), is_navigable=True) + # }) + + model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}, associations={assoc, assoc2}) generator = SpringEntityGenerator( model, diff --git a/besser/generators/spring/templates/entity.j2 b/besser/generators/spring/templates/entity.j2 index bd38e911a..298576062 100644 --- a/besser/generators/spring/templates/entity.j2 +++ b/besser/generators/spring/templates/entity.j2 @@ -8,7 +8,7 @@ import {{ import }}; @MappedSuperclass {% else %} @Entity -@Table(name="{{ table_name }}") +@Table(name = "{{ table_name }}") {% endif -%} public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent %} extends {{parent}}{% endif %} { {% for attribute in attributes -%} @@ -28,9 +28,45 @@ public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent {%- endif %} {% endfor -%} + {% for rel in relations -%} + + {% if rel.relation == "OneToOne" %} + @OneToOne{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} + {% if rel.owning -%} + @JoinColumn(name = "{{ rel.join_column }}") + {%- endif %} + {% endif -%} + + {% if rel.relation == "OneToMany" %} + @OneToMany{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} + {% if rel.owning -%} + @JoinColumn(name = "{{ rel.join_column }}") + {%- endif %} + {% endif -%} + + {% if rel.relation == "ManyToOne" %} + @ManyToOne + @JoinColumn(name = "{{ rel.join_column }}") + {% endif -%} + + {% if rel.relation == "ManyToMany" %} + @ManyToMany{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} + @JoinTable(name = "{{ rel.assoc }}", + joinColumns = @JoinColumn(name = "{{ rel.to_snake_case(rel.source_cls) }}_id"), + inverseJoinColumns = @JoinColumn(name = "{{ rel.to_snake_case(rel.target_cls) }}_id")) + {% endif -%} + + {%- if rel.is_list -%} + private {{ rel.type }} {{ rel.target_property }} = new ArrayList<>(); + {% else -%} + private {{ rel.type }} {{ rel.target_property }}; + {% endif -%} + {%- endfor -%} + {% if not is_abstract %} public {{ cls.name }}() { } + {% if attributes -%} public {{ cls.name }}( {%- for attribute in attributes -%} {{ attribute.type }} {{ attribute.name }}{% if not loop.last %}, {% endif %} @@ -39,6 +75,7 @@ public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent this.{{ attribute.name }} = {{ attribute.name }}; {%- endfor %} } + {%- endif %} {% endif -%} {% for attribute in attributes %} From 13c9643803fdfdc338bb578fa14d11c2f0f0afc1 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Thu, 12 Mar 2026 09:05:17 +0100 Subject: [PATCH 05/16] fix relation --- .../generated/{ => entity}/Computer.java | 12 +- .../spring/generated/{ => entity}/Hand.java | 2 +- .../spring/generated/{ => entity}/Role.java | 0 .../spring/generated/{ => entity}/Thing.java | 0 .../spring/generated/{ => entity}/User.java | 2 +- besser/generators/spring/generator.py | 221 ----- besser/generators/spring/gh.py | 745 ++++++++++++++ besser/generators/spring/proba.py | 31 +- .../spring/spring_entity_generator.py | 933 ++++-------------- besser/generators/spring/templates/entity.j2 | 90 -- .../spring/templates/entity.java.j2 | 127 ++- 11 files changed, 1057 insertions(+), 1106 deletions(-) rename besser/generators/spring/generated/{ => entity}/Computer.java (61%) rename besser/generators/spring/generated/{ => entity}/Hand.java (74%) rename besser/generators/spring/generated/{ => entity}/Role.java (100%) rename besser/generators/spring/generated/{ => entity}/Thing.java (100%) rename besser/generators/spring/generated/{ => entity}/User.java (97%) delete mode 100644 besser/generators/spring/generator.py create mode 100644 besser/generators/spring/gh.py delete mode 100644 besser/generators/spring/templates/entity.j2 diff --git a/besser/generators/spring/generated/Computer.java b/besser/generators/spring/generated/entity/Computer.java similarity index 61% rename from besser/generators/spring/generated/Computer.java rename to besser/generators/spring/generated/entity/Computer.java index ec4e312dd..59e80d640 100644 --- a/besser/generators/spring/generated/Computer.java +++ b/besser/generators/spring/generated/entity/Computer.java @@ -11,13 +11,11 @@ @Table(name = "computers") public class Computer extends Thing { - @OneToOne - @JoinColumn(name = "user_id") - private User owner; - - @ManyToOne - @JoinColumn(name = "user_id") - private User ownerr; + @ManyToMany + @JoinTable(name = "many_to_many", + joinColumns = @JoinColumn(name = "computer_id"), + inverseJoinColumns = @JoinColumn(name = "user_id")) + private List owners = new ArrayList<>(); public Computer() { } diff --git a/besser/generators/spring/generated/Hand.java b/besser/generators/spring/generated/entity/Hand.java similarity index 74% rename from besser/generators/spring/generated/Hand.java rename to besser/generators/spring/generated/entity/Hand.java index 03b9752b8..228c9d9d0 100644 --- a/besser/generators/spring/generated/Hand.java +++ b/besser/generators/spring/generated/entity/Hand.java @@ -1,5 +1,5 @@ package spring.entities; public enum Hand { - LEFT, RIGHT + RIGHT, LEFT } \ No newline at end of file diff --git a/besser/generators/spring/generated/Role.java b/besser/generators/spring/generated/entity/Role.java similarity index 100% rename from besser/generators/spring/generated/Role.java rename to besser/generators/spring/generated/entity/Role.java diff --git a/besser/generators/spring/generated/Thing.java b/besser/generators/spring/generated/entity/Thing.java similarity index 100% rename from besser/generators/spring/generated/Thing.java rename to besser/generators/spring/generated/entity/Thing.java diff --git a/besser/generators/spring/generated/User.java b/besser/generators/spring/generated/entity/User.java similarity index 97% rename from besser/generators/spring/generated/User.java rename to besser/generators/spring/generated/entity/User.java index 3098d8c16..b65a6107f 100644 --- a/besser/generators/spring/generated/User.java +++ b/besser/generators/spring/generated/entity/User.java @@ -24,7 +24,7 @@ public class User { @Column(name = "first_name") public String firstName = "John"; - @OneToMany(mappedBy = "ownerr") + @ManyToMany(mappedBy = "owners") private List computers = new ArrayList<>(); diff --git a/besser/generators/spring/generator.py b/besser/generators/spring/generator.py deleted file mode 100644 index 42fc7ac21..000000000 --- a/besser/generators/spring/generator.py +++ /dev/null @@ -1,221 +0,0 @@ -from collections import defaultdict -import os -import re -from typing import List -from jinja2 import Environment, FileSystemLoader -from besser.BUML.metamodel.structural import DomainModel -from besser.BUML.metamodel.structural.structural import BinaryAssociation, Class, Enumeration, Property, StringType, BooleanType, DateTimeType, DateType, FloatType, IntegerType, TimeDeltaType, TimeType -from besser.generators import GeneratorInterface - -class SpringEntityGenerator(GeneratorInterface): - - JAVA_TYPES = { - StringType.name: "String", - BooleanType.name: "Boolean", - IntegerType.name: "Integer", - FloatType.name: "Float", - DateType.name: "LocalDate", - DateTimeType.name: "LocalDateTime", - TimeType.name: "LocalDateTime", - TimeDeltaType.name: "Duration" - } - - def __init__(self, model: DomainModel, - output_dir: str = "./generated/entities", - package_name: str = "com.example.entities"): - super().__init__(model, output_dir) - - self.package_name: str = package_name - self.enumerations: set[Enumeration] = model.get_enumerations() - - def generate(self): - model: DomainModel = self.model - assoc_map: defaultdict[str, List[BinaryAssociation]] = defaultdict(list) - - for assoc in model.associations: - end1, end2 = list(assoc.ends) - assoc_map[end1.type.name].append(assoc) - assoc_map[end2.type.name].append(assoc) - - for enum in model.get_enumerations(): - self._generate_enum_file(enum) - - for cls in model.classes_sorted_by_inheritance(): - self._generate_class_file(cls, assoc_map[cls.name]) - - def _generate_class_file(self, cls: Class, assocs_for_class: List[BinaryAssociation]): - file_path = self.build_generation_path(file_name=f"{cls.name.capitalize()}.java") - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path)) - entity_template = env.get_template("entity.j2") - - imports: set[str] = self._get_default_imports().union(self._get_specific_imports_for_class(cls)) - - context = { - "cls": cls, - "package_name": self.package_name, - "imports": sorted(list(imports)), - "is_abstract": cls.is_abstract, - "table_name": self._pluralize(cls.name.lower()), - "parent": cls.parents().pop().name if cls.parents() else None, - "attributes": sorted( - self._prepare_attributes(cls), - key=lambda a: (not a["is_id"], not a["is_enum"], not a["is_list"], a["name"]) - ), - "relations": self._prepare_relations(cls, assocs_for_class), - } - - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = entity_template.render(**context) - f.write(generated_code) - - def _prepare_attributes(self, cls: Class) -> List[object]: - attributes: List[object] = [] - - for attr in cls.attributes: - is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) - is_list: bool = attr.multiplicity.max != 1 - attr_type = None - - if is_enum: - attr_type = attr.type.name - else: - attr_type = self.JAVA_TYPES[attr.type.name] - if is_list: - attr_type = f"List<{attr_type}>" - - attributes.append({ - "is_id": attr.is_id, - "column_name": self._to_snake_case(attr.name), - "is_optional": attr.is_optional, - "is_enum": is_enum, - "is_list": is_list, - "visibility": attr.visibility, - "type": attr_type, - "name": attr.name, - "default_value": f"\"{attr.default_value}\"" if attr.default_value and attr.type.name == "str" and not is_list else attr.default_value - }) - - return attributes - - def _prepare_relations(self, cls: Class, assocs_for_class: List[BinaryAssociation]) -> List[object]: - relations: List[object] = [] - - for assoc in assocs_for_class: - ends: List[Property] = list(assoc.ends) - end1, end2 = ends - - if end1.type.name == cls.name and end2.is_navigable: - source: Property = end1 - target: Property = end2 - elif end2.type.name == cls.name and end1.is_navigable: - source: Property = end2 - target: Property = end1 - else: - continue - - bidirectional: bool = source.is_navigable and target.is_navigable - source_many = source.multiplicity.max != 1 - target_many = target.multiplicity.max != 1 - - relation: str = None - mapped_by: str = None - owning: bool = True - - if not source_many and not target_many: - relation = "OneToOne" - if bidirectional: - mapped_by = source.name - elif not source_many and target_many: - relation = "OneToMany" - if bidirectional: - owning = False - mapped_by = source.name - elif source_many and not target_many: - relation = "ManyToOne" - elif source_many and target_many: - relation = "ManyToMany" - if bidirectional: - mapped_by = source.name - - relations.append({ - "assoc": assoc.name, - "source_property": source.name, - "target_property": target.name, - "source_cls": source.type.name, - "target_cls": target.type.name, - "relation": relation, - "mapped_by": mapped_by, - "join_column": f"{self._to_snake_case(target.type.name)}_id", - "owning": owning, - "is_list": target_many, - "type": f"List<{target.type.name}>" if target_many else target.type.name, - "to_snake_case": self._to_snake_case - }) - - return relations - - def _generate_enum_file(self, enum: Enumeration): - file_path = self.build_generation_path(file_name=f"{enum.name.capitalize()}.java") - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path)) - entity_template = env.get_template("enum.java.j2") - - context = { - "package_name": self.package_name, - "name": enum.name.capitalize(), - "literals": enum.literals - } - - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = entity_template.render(**context) - f.write(generated_code) - - def _get_default_imports(self) -> set[str]: - return set([ - "javax.persistence.Entity", - "javax.persistence.Table", - "javax.persistence.Id", - "javax.persistence.GeneratedValue", - "javax.persistence.GenerationType", - "javax.persistence.Column", - ]) - - def _get_specific_imports_for_class(self, cls: Class) -> set[str]: - imports: set[str] = set() - - if cls.is_abstract: - imports.add("javax.persistence.MappedSuperclass") - - if any(any(attr.type.name == enum.name for enum in self.enumerations) for attr in cls.attributes): - imports.add("javax.persistence.Enumerated") - imports.add("javax.persistence.EnumType") - - for attr in cls.attributes: - if attr.type.name == DateType.name: - imports.add("java.time.LocalDate") - elif attr.type.name in [DateTimeType.name, TimeType.name]: - imports.add("java.time.LocalDateTime") - elif attr.type.name == TimeDeltaType.name: - imports.add("java.time.Duration") - - if attr.multiplicity.max != 1: - imports.add("java.util.List") - imports.add("java.util.ArrayList") - imports.add("java.util.Arrays") - - return imports - - @staticmethod - def _pluralize(name: str) -> str: - if name.endswith("y"): - return name[:-1] + "ies" - elif name.endswith(("s", "x", "z", "ch", "sh")): - return name + "es" - else: - return name + "s" - - @staticmethod - def _to_snake_case(name: str) -> str: - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() diff --git a/besser/generators/spring/gh.py b/besser/generators/spring/gh.py new file mode 100644 index 000000000..ce687a33c --- /dev/null +++ b/besser/generators/spring/gh.py @@ -0,0 +1,745 @@ +""" +Spring Entity Code Generator for BESSER B-UML Models +Generates Spring JPA Entity classes from BESSER structural models +With full support for JPA/Hibernate associations and BESSER metamodel features + +BESSER dokumentacija: +https://besser.readthedocs.io/en/latest/api/BUML/metamodel/api_structural.html + +Features: +- Apstraktne klase (abstract classes) +- Enumeracije (enumerations) +- ID atributi (@Id annotation) +- Navigable fields +- Opcioni atributi +- Visibility modifiers (public, private, protected) +- Readonly atributi (@Immutable) +- Izvedeni atributi (derived) +- Updatable/Insertable constraints +- Opisi (descriptions) i metadata +- Unique constraints +- Inheritance strategies + +Author: Generator for BESSER-PEARL/BESSER +""" + +from typing import Set, Dict, List, Optional +from pathlib import Path +import re +from enum import Enum +from jinja2 import Template, Environment, FileSystemLoader + +from besser.BUML.metamodel.structural.structural import ( + Class, Association, DomainModel, Property, Type, Multiplicity, Enumeration +) +from besser.generators.generator_interface import GeneratorInterface + + +class SpringEntityGenerator(GeneratorInterface): + """ + Generator za Spring Entity klase iz BESSER B-UML modela. + + Podržava sve BESSER metamodel koncepte: + - Apstraktne klase + - Enumeracije + - ID atributi + - Navigable polja + - Opcioni atributi + - Visibility modifiers + - Readonly atributi + - Izvedeni atributi + - Updatable/Insertable constraints + - Inheritance + - Sva JPA/Hibernate asocijacije + + Primer korišćenja: + ```python + from besser.BUML.metamodel.structural.structural import Diagram + from spring_entity_generator import SpringEntityGenerator + + diagram = Diagram() + # ... dodaj klase, atribute, relacije ... + + generator = SpringEntityGenerator( + diagram, + output_path="./src/main/java", + templates_path="./templates" + ) + generator.generate() + ``` + """ + + # Java tipovi mapiranje + JAVA_TYPE_MAPPING = { + 'String': 'String', + 'Integer': 'Integer', + 'Int': 'Integer', + 'Long': 'Long', + 'Double': 'Double', + 'Float': 'Float', + 'Boolean': 'Boolean', + 'Bool': 'Boolean', + 'LocalDate': 'LocalDate', + 'LocalDateTime': 'LocalDateTime', + 'Date': 'LocalDateTime', + 'BigDecimal': 'BigDecimal', + 'UUID': 'UUID', + } + + def __init__(self, model, output_path: str = "./generated/entities", + package_name: str = "com.example.entity", + templates_path: str = "./templates"): + """ + Inicijalizuj generator + + Args: + model: B-UML Diagram model + output_path: Putanja gde će biti sačuvane generirane klase + package_name: Java package za generirane klase + templates_path: Putanja do Jinja2 template fajlova + """ + super().__init__(model) + self.output_path = Path(output_path) + self.package_name = package_name + self.templates_path = Path(templates_path) + self.output_path.mkdir(parents=True, exist_ok=True) + + # Inicijalizuj Jinja2 okruženje + self.jinja_env = Environment( + loader=FileSystemLoader(str(self.templates_path)), + trim_blocks=True, + lstrip_blocks=True + ) + + # Mapiranje B-UML klasa sa njihovim asocijacijama + self.associations_by_class: Dict[str, List[Association]] = {} + self.enumerations: Dict[str, Enumeration] = {} + + self._build_association_map() + self._build_enumerations_map() + + def _build_association_map(self): + """Gradi mapu asocijacija za svaku klasu""" + if not hasattr(self.model, 'classes') or self.model.classes is None: + return + + for cls in self.model.classes: + self.associations_by_class[cls.name] = [] + + for cls in self.model.classes: + if hasattr(cls, 'association_ends') and cls.association_ends: + for assoc_end in cls.association_ends: + if hasattr(assoc_end, 'association'): + assoc = assoc_end.association + if assoc not in self.associations_by_class[cls.name]: + self.associations_by_class[cls.name].append(assoc) + + def _build_enumerations_map(self): + """Gradi mapu enumeracija""" + if not hasattr(self.model, 'enumerations') or self.model.enumerations is None: + return + + for enum in self.model.enumerations: + self.enumerations[enum.name] = enum + + def generate(self) -> List[str]: + """ + Generiši sve Spring Entity klase i enumeracije + + Returns: + Lista putanja generianih datoteka + """ + generated_files = [] + + # Generiši enumeracije + if hasattr(self.model, 'enumerations') and self.model.enumerations: + for enum in self.model.enumerations: + try: + file_path = self._generate_enumeration_class(enum) + generated_files.append(file_path) + except Exception as e: + print(f"Greška pri generisanju enumeracije {enum.name}: {e}") + + # Generiši entitete + if not hasattr(self.model, 'classes') or self.model.classes is None: + return generated_files + + for cls in self.model.classes: + try: + file_path = self._generate_entity_class(cls) + generated_files.append(file_path) + except Exception as e: + print(f"Greška pri generisanju klase {cls.name}: {e}") + + return generated_files + + def _generate_enumeration_class(self, enum: Enumeration) -> str: + """Generiši Java enumeraciju""" + enum_values = [] + + if hasattr(enum, 'literals') and enum.literals: + for literal in enum.literals: + enum_values.append(literal.name if hasattr(literal, 'name') else str(literal)) + + enum_code = f"""package {self.package_name}; + +/** + * Enumeracija {enum.name} + * + * Generirano automatski od BESSER B-UML modela + * + * @author Spring Entity Generator + * @version 1.0 + */ +public enum {enum.name} {{ +""" + + for i, value in enumerate(enum_values): + enum_code += f" {value}" + if i < len(enum_values) - 1: + enum_code += ",\n" + else: + enum_code += ";\n" + + enum_code += f""" + /** + * Pronađi enumeraciju po imenu + * + * @param value String vrednost + * @return {enum.name} enumeracija + */ + public static {enum.name} fromValue(String value) {{ + try {{ + return valueOf(value.toUpperCase()); + }} catch (IllegalArgumentException e) {{ + return null; + }} + }} +}} +""" + + # Napiši datoteku + file_path = self.output_path / f"{enum.name}.java" + with open(file_path, 'w', encoding='utf-8') as f: + f.write(enum_code) + + return str(file_path) + + def _generate_entity_class(self, buml_class: Class) -> str: + """Generiši jednu Spring Entity klasu""" + + # Pripremi podatke za template + context = { + 'class_name': buml_class.name, + 'package_name': self.package_name, + 'table_name': self._to_snake_case(buml_class.name), + 'parent': self._get_parent_class(buml_class), + 'is_abstract': self._is_abstract_class(buml_class), + 'description': self._get_description(buml_class), + 'imports': self._generate_imports(buml_class), + 'attributes': self._prepare_attributes(buml_class), + 'relations': self._prepare_relations(buml_class), + 'unique_constraints': self._prepare_unique_constraints(buml_class), + 'constructor_params': self._generate_constructor_params(buml_class), + 'constructor_params_list': self._generate_constructor_params_list(buml_class), + 'constructor_assignments': self._generate_constructor_assignments(buml_class), + 'getters_setters': self._prepare_getters_setters(buml_class), + } + + # Učitaj i primeni template + template = self.jinja_env.get_template('entity_template.j2') + entity_code = template.render(**context) + + # Napiši datoteku + file_path = self.output_path / f"{buml_class.name}.java" + with open(file_path, 'w', encoding='utf-8') as f: + f.write(entity_code) + + return str(file_path) + + def _is_abstract_class(self, buml_class: Class) -> bool: + """Proverite da li je klasa apstraktna""" + return hasattr(buml_class, 'is_abstract') and buml_class.is_abstract + + def _get_parent_class(self, buml_class: Class) -> Optional[str]: + """Pronađi parent klasu ako postoji generalizacija""" + if hasattr(buml_class, 'parents') and buml_class.parents: + parents = buml_class.parents + if parents: + parent = list(parents)[0] + return parent.name + return None + + def _get_description(self, buml_class: Class) -> Optional[str]: + """Preuzmi opis klase""" + if hasattr(buml_class, 'metadata') and buml_class.metadata: + if hasattr(buml_class.metadata, 'description'): + return buml_class.metadata.description + return None + + def _get_visibility(self, element) -> str: + """Preuzmi visibility modifier""" + visibility = "private" # Default + + if hasattr(element, 'visibility'): + vis = element.visibility + if vis is None: + visibility = "public" + elif isinstance(vis, str): + vis_lower = vis.lower() + if 'public' in vis_lower: + visibility = "public" + elif 'protected' in vis_lower: + visibility = "protected" + elif 'private' in vis_lower: + visibility = "private" + else: + # Ako je VisibilityKind enum + try: + if hasattr(vis, 'name'): + vis_name = vis.name.lower() + if 'public' in vis_name: + visibility = "public" + elif 'protected' in vis_name: + visibility = "protected" + elif 'private' in vis_name: + visibility = "private" + except: + pass + + return visibility + + def _generate_imports(self, buml_class: Class) -> List[str]: + """Generiši potrebne Java importe""" + imports = set([ + "jakarta.persistence.Entity", + "jakarta.persistence.Table", + "jakarta.persistence.Id", + "jakarta.persistence.GeneratedValue", + "jakarta.persistence.GenerationType", + "jakarta.persistence.Column", + "jakarta.persistence.OneToOne", + "jakarta.persistence.OneToMany", + "jakarta.persistence.ManyToOne", + "jakarta.persistence.ManyToMany", + "jakarta.persistence.JoinColumn", + "jakarta.persistence.JoinTable", + "jakarta.persistence.FetchType", + "jakarta.persistence.CascadeType", + "jakarta.persistence.Enumerated", + "jakarta.persistence.EnumType", + "jakarta.persistence.UniqueConstraint", + "java.time.LocalDate", + "java.time.LocalDateTime", + "java.util.HashSet", + "java.util.Set", + "java.util.Objects", + ]) + + # Dodaj inheritance import ako je potrebna + if self._is_abstract_class(buml_class): + imports.add("jakarta.persistence.Inheritance") + imports.add("jakarta.persistence.InheritanceType") + + # Dodaj importe na osnovu atributa + if buml_class.attributes: + for attr in buml_class.attributes: + if self._is_readonly_attribute(attr): + imports.add("org.hibernate.annotations.Immutable") + + if self._is_enum_attribute(attr): + enum_type = self._get_enum_name(attr.type) + imports.add(f"{self.package_name}.{enum_type}") + elif hasattr(attr.type, 'name'): + type_name = attr.type.name + if 'BigDecimal' in type_name: + imports.add("java.math.BigDecimal") + if 'UUID' in type_name: + imports.add("java.util.UUID") + + return sorted(list(imports)) + + def _is_readonly_attribute(self, attr: Property) -> bool: + """Proverite da li je atribut readonly""" + return hasattr(attr, 'is_read_only') and attr.is_read_only + + def _is_derived_attribute(self, attr: Property) -> bool: + """Proverite da li je atribut izvedena vrednost""" + return hasattr(attr, 'is_derived') and attr.is_derived + + def _is_enum_attribute(self, attr: Property) -> bool: + """Proverite da li je atribut enumeracija""" + if hasattr(attr, 'type') and attr.type: + type_name = attr.type.name if hasattr(attr.type, 'name') else str(attr.type) + return type_name in self.enumerations + return False + + def _get_enum_name(self, attr_type) -> str: + """Preuzmi naziv enumeracije""" + if hasattr(attr_type, 'name'): + return attr_type.name + return str(attr_type) + + def _prepare_attributes(self, buml_class: Class) -> List[Dict]: + """Pripremi podatke za atribute""" + attributes = [] + + if not buml_class.attributes: + return attributes + + for attr in buml_class.attributes: + if self._is_association_end(attr): + continue + + # Proverite da li je ovo ID polje + is_id = hasattr(attr, 'is_id') and attr.is_id + + # Visibility + visibility = self._get_visibility(attr) + + # Readonly + is_readonly = self._is_readonly_attribute(attr) + + # Derived + is_derived = self._is_derived_attribute(attr) + + # Enumeracija + is_enum = self._is_enum_attribute(attr) + + java_type = self._map_type_to_java(attr.type) + column_name = self._to_snake_case(attr.name) + + # Nullable + nullable = True + if hasattr(attr, 'lower') and attr.lower == 1: + nullable = False + + # Length + length = None + if java_type == 'String': + length = 255 + + # Unique + unique = False + if 'email' in attr.name.lower() or 'username' in attr.name.lower(): + unique = True + + # Opis + description = None + if hasattr(attr, 'metadata') and attr.metadata: + if hasattr(attr.metadata, 'description'): + description = attr.metadata.description + + # Default vrednost + default_value = None + if hasattr(attr, 'default_value'): + default_value = attr.default_value + + # Updatable/Insertable + updatable = None + insertable = None + + if is_readonly: + updatable = False + + if is_id or is_derived: + insertable = False + + # Enum vrednosti + enum_values = None + if is_enum: + enum_name = self._get_enum_name(attr.type) + if enum_name in self.enumerations: + enum = self.enumerations[enum_name] + if hasattr(enum, 'literals'): + enum_values = [lit.name if hasattr(lit, 'name') else str(lit) for lit in enum.literals] + + attributes.append({ + 'name': attr.name, + 'java_type': java_type, + 'column_name': column_name, + 'visibility': visibility, + 'nullable': nullable, + 'unique': unique, + 'length': length, + 'is_id': is_id, + 'is_enum': is_enum, + 'is_readonly': is_readonly, + 'is_derived': is_derived, + 'description': description, + 'default_value': default_value, + 'updatable': updatable, + 'insertable': insertable, + 'enum_values': ", ".join(enum_values) if enum_values else None + }) + + return attributes + + def _prepare_relations(self, buml_class: Class) -> List[Dict]: + """Pripremi podatke za relacije""" + relations = [] + + if buml_class.name not in self.associations_by_class: + return relations + + for assoc in self.associations_by_class[buml_class.name]: + source_end = None + target_end = None + + if hasattr(assoc, 'ends') and assoc.ends: + for end in assoc.ends: + if hasattr(end, 'type') and end.type == buml_class: + source_end = end + else: + target_end = end + + if not source_end or not target_end: + continue + + relation_type = self._determine_relation_type(source_end, target_end) + target_class = target_end.type.name if hasattr(target_end, 'type') else "Unknown" + field_name = self._to_camel_case_first_lower(target_class) + plural_name = self._pluralize(field_name) + plural_name_capitalized = self._capitalize_first(plural_name) + + # Visibility + visibility = self._get_visibility(source_end) + + # Navigable + is_navigable = hasattr(source_end, 'is_navigable') and source_end.is_navigable + + # Owning side + is_owning_side = relation_type in ["ManyToOne", "ManyToMany"] + + # Cascade types + cascade_types = ["PERSIST", "MERGE"] + if relation_type == "OneToMany": + cascade_types = ["ALL"] + + # Fetch type + fetch_type = "LAZY" + + # Optional + is_optional = True + if relation_type in ["ManyToOne"]: + if hasattr(target_end, 'lower') and target_end.lower == 1: + is_optional = False + + # Opis + description = None + if hasattr(source_end, 'metadata') and source_end.metadata: + if hasattr(source_end.metadata, 'description'): + description = source_end.metadata.description + + side_description = "OWNING SIDE" if is_owning_side else "INVERSE SIDE" + cascade_types_str = ", ".join(cascade_types) + + relation_data = { + 'type': relation_type, + 'target_class': target_class, + 'field_name': field_name, + 'plural_name': plural_name, + 'plural_name_capitalized': plural_name_capitalized, + 'join_column_name': self._to_snake_case(field_name) + '_id', + 'mapped_by': self._to_camel_case_first_lower(buml_class.name), + 'source_class': buml_class.name, + 'join_table_name': f"{self._to_snake_case(buml_class.name)}_{self._to_snake_case(target_class)}", + 'source_join_column': f"{self._to_snake_case(buml_class.name)}_id", + 'target_join_column': f"{self._to_snake_case(target_class)}_id", + 'visibility': visibility, + 'is_owning_side': is_owning_side, + 'is_navigable': is_navigable, + 'cascade_types': cascade_types, + 'cascade_types_str': cascade_types_str, + 'fetch_type': fetch_type, + 'is_optional': is_optional, + 'side_description': side_description, + 'description': description + } + + relations.append(relation_data) + + return relations + + def _prepare_unique_constraints(self, buml_class: Class) -> List[Dict]: + """Pripremi podatke za unique constraints""" + constraints = [] + + if not buml_class.attributes: + return constraints + + unique_attrs = [] + for attr in buml_class.attributes: + if self._is_association_end(attr): + continue + + if 'email' in attr.name.lower() or 'username' in attr.name.lower(): + unique_attrs.append(attr.name) + + if unique_attrs: + constraint = { + 'name': f"uk_{self._to_snake_case(buml_class.name)}_{'_'.join([self._to_snake_case(a) for a in unique_attrs])}", + 'columns': ', '.join([f'"{self._to_snake_case(a)}"' for a in unique_attrs]) + } + constraints.append(constraint) + + return constraints + + def _generate_constructor_params(self, buml_class: Class) -> str: + """Generiši parametre za konstruktor""" + all_attrs = self._get_all_attributes(buml_class) + + if not all_attrs: + return "" + + params = [] + for attr in all_attrs: + java_type = self._map_type_to_java(attr.type) + attr_name = attr.name if hasattr(attr, 'name') else "unknown" + params.append(f"{java_type} {attr_name}") + + return ", ".join(params) + + def _generate_constructor_params_list(self, buml_class: Class) -> List[Dict]: + """Generiši listu parametara sa tipom""" + all_attrs = self._get_all_attributes(buml_class) + params = [] + + for attr in all_attrs: + java_type = self._map_type_to_java(attr.type) + attr_name = attr.name if hasattr(attr, 'name') else "unknown" + params.append({ + 'name': attr_name, + 'type': java_type + }) + + return params + + def _generate_constructor_assignments(self, buml_class: Class) -> List[str]: + """Generiši dodele vrednosti u konstruktoru""" + all_attrs = self._get_all_attributes(buml_class) + assignments = [] + + for attr in all_attrs: + attr_name = attr.name if hasattr(attr, 'name') else "unknown" + assignments.append(f"this.{attr_name} = {attr_name};") + + return assignments + + def _get_all_attributes(self, buml_class: Class) -> List: + """Preuzmi sve atribute (i obične i relacijske)""" + all_attrs = [] + + if buml_class.attributes: + for attr in buml_class.attributes: + if not self._is_association_end(attr): + all_attrs.append(attr) + + if buml_class.name in self.associations_by_class: + for assoc in self.associations_by_class[buml_class.name]: + for end in assoc.ends: + if hasattr(end, 'type') and end.type != buml_class: + all_attrs.append(end) + break + + return all_attrs + + def _prepare_getters_setters(self, buml_class: Class) -> List[Dict]: + """Pripremi podatke za getter i setter metode""" + getters_setters = [] + + if not buml_class.attributes: + return getters_setters + + for attr in buml_class.attributes: + if self._is_association_end(attr): + continue + + java_type = self._map_type_to_java(attr.type) + attr_name = attr.name if hasattr(attr, 'name') else "unknown" + method_name = self._capitalize_first(attr_name) + visibility = self._get_visibility(attr) + + getters_setters.append({ + 'java_type': java_type, + 'attribute_name': attr_name, + 'method_name': method_name, + 'visibility': visibility + }) + + return getters_setters + + def _is_association_end(self, attr: Property) -> bool: + """Proverite da li je atribut asocijacijski kraj""" + if hasattr(attr, 'type') and attr.type: + type_name = attr.type.name if hasattr(attr.type, 'name') else str(attr.type) + return type_name[0].isupper() and type_name not in self.JAVA_TYPE_MAPPING + return False + + def _map_type_to_java(self, buml_type) -> str: + """Mapira B-UML tip na Java tip""" + if buml_type is None: + return "Object" + + type_name = buml_type.name if hasattr(buml_type, 'name') else str(buml_type) + + if type_name in self.enumerations: + return type_name + + return self.JAVA_TYPE_MAPPING.get(type_name, type_name) + + def _determine_relation_type(self, source_end, target_end) -> str: + """Odredi tip relacije na osnovu multipliciteta""" + source_mult = self._get_multiplicity(source_end) + target_mult = self._get_multiplicity(target_end) + + if target_mult in ["0..1", "1"]: + if source_mult in ["0..*", "*"]: + return "ManyToOne" + return "OneToOne" + elif target_mult in ["0..*", "*"]: + if source_mult in ["0..*", "*"]: + return "ManyToMany" + return "OneToMany" + + return "ManyToOne" + + def _get_multiplicity(self, end) -> str: + """Pronađi multipliciteta asocijacionog kraja""" + if hasattr(end, 'multiplicity') and end.multiplicity: + mult = end.multiplicity + if hasattr(mult, 'lower') and hasattr(mult, 'upper'): + if mult.upper == -1: + return f"{mult.lower}..*" + return f"{mult.lower}..{mult.upper}" + return "0..1" + + @staticmethod + def _to_snake_case(name: str) -> str: + """Konvertuje CamelCase u snake_case""" + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + @staticmethod + def _to_camel_case_first_lower(name: str) -> str: + """Konvertuje CamelCase u camelCase""" + if not name: + return name + return name[0].lower() + name[1:] + + @staticmethod + def _capitalize_first(name: str) -> str: + """Velikim baš prvi karakter""" + if not name: + return name + return name[0].upper() + name[1:] + + @staticmethod + def _pluralize(name: str) -> str: + """Pluralizuje ime""" + if name.endswith('y'): + return name[:-1] + 'ies' + elif name.endswith(('s', 'x', 'z', 'ch', 'sh')): + return name + 'es' + else: + return name + 's' \ No newline at end of file diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index 54855fc76..438698018 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -9,12 +9,11 @@ from besser.BUML.metamodel.structural.structural import ( BinaryAssociation, DateTimeType, DomainModel, Class, FloatType, Generalization, IntegerType, Multiplicity, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral ) -from besser.generators.spring.generator import SpringEntityGenerator - +from besser.generators.spring.spring_entity_generator import SpringEntityGenerator def proba_jednostavna_klasa(): current_dir = Path(__file__).parent - output_dir = current_dir / "generated" + output_dir = current_dir / "generated/entity" output_dir.mkdir(exist_ok=True) role = Enumeration(name="Role", literals={EnumerationLiteral(name="USER"), EnumerationLiteral(name="ADMIN")}) @@ -36,27 +35,27 @@ def proba_jednostavna_klasa(): gen1 = Generalization(general=thing, specific=computer) - assoc = BinaryAssociation(name="onetoone", ends={ - Property(name="computer", type=computer, multiplicity=Multiplicity(1, 1), is_navigable=False), - Property(name="owner", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True) - }) + # assoc = BinaryAssociation(name="onetoone", ends={ + # Property(name="computer", type=computer, multiplicity=Multiplicity(1, 1), is_navigable=True), + # Property(name="owner", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True) + # }) - assoc2 = BinaryAssociation(name="onetomany", ends={ - Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True), - Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True) - }) + # assoc2 = BinaryAssociation(name="onetomany", ends={ + # Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True), + # Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True) + # }) # assoc2 = BinaryAssociation(name="manytoone", ends={ # Property(name="computerss", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), # Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True) # }) - # assoc = BinaryAssociation(name="ManyToMany", ends={ - # Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), - # Property(name="owners", type=user, multiplicity=Multiplicity(0, "*"), is_navigable=True) - # }) + assoc = BinaryAssociation(name="ManyToMany", ends={ + Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), + Property(name="owners", type=user, multiplicity=Multiplicity(0, "*"), is_navigable=True) + }) - model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}, associations={assoc, assoc2}) + model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}, associations={assoc}) generator = SpringEntityGenerator( model, diff --git a/besser/generators/spring/spring_entity_generator.py b/besser/generators/spring/spring_entity_generator.py index ce687a33c..09a537d58 100644 --- a/besser/generators/spring/spring_entity_generator.py +++ b/besser/generators/spring/spring_entity_generator.py @@ -1,745 +1,234 @@ -""" -Spring Entity Code Generator for BESSER B-UML Models -Generates Spring JPA Entity classes from BESSER structural models -With full support for JPA/Hibernate associations and BESSER metamodel features - -BESSER dokumentacija: -https://besser.readthedocs.io/en/latest/api/BUML/metamodel/api_structural.html - -Features: -- Apstraktne klase (abstract classes) -- Enumeracije (enumerations) -- ID atributi (@Id annotation) -- Navigable fields -- Opcioni atributi -- Visibility modifiers (public, private, protected) -- Readonly atributi (@Immutable) -- Izvedeni atributi (derived) -- Updatable/Insertable constraints -- Opisi (descriptions) i metadata -- Unique constraints -- Inheritance strategies - -Author: Generator for BESSER-PEARL/BESSER -""" - -from typing import Set, Dict, List, Optional -from pathlib import Path +from collections import defaultdict +import os import re -from enum import Enum -from jinja2 import Template, Environment, FileSystemLoader - -from besser.BUML.metamodel.structural.structural import ( - Class, Association, DomainModel, Property, Type, Multiplicity, Enumeration -) -from besser.generators.generator_interface import GeneratorInterface - +from typing import List +from jinja2 import Environment, FileSystemLoader +from besser.BUML.metamodel.structural import DomainModel +from besser.BUML.metamodel.structural.structural import BinaryAssociation, Class, Enumeration, Property, StringType, BooleanType, DateTimeType, DateType, FloatType, IntegerType, TimeDeltaType, TimeType +from besser.generators import GeneratorInterface class SpringEntityGenerator(GeneratorInterface): - """ - Generator za Spring Entity klase iz BESSER B-UML modela. - - Podržava sve BESSER metamodel koncepte: - - Apstraktne klase - - Enumeracije - - ID atributi - - Navigable polja - - Opcioni atributi - - Visibility modifiers - - Readonly atributi - - Izvedeni atributi - - Updatable/Insertable constraints - - Inheritance - - Sva JPA/Hibernate asocijacije - - Primer korišćenja: - ```python - from besser.BUML.metamodel.structural.structural import Diagram - from spring_entity_generator import SpringEntityGenerator - - diagram = Diagram() - # ... dodaj klase, atribute, relacije ... - - generator = SpringEntityGenerator( - diagram, - output_path="./src/main/java", - templates_path="./templates" - ) - generator.generate() - ``` - """ - - # Java tipovi mapiranje - JAVA_TYPE_MAPPING = { - 'String': 'String', - 'Integer': 'Integer', - 'Int': 'Integer', - 'Long': 'Long', - 'Double': 'Double', - 'Float': 'Float', - 'Boolean': 'Boolean', - 'Bool': 'Boolean', - 'LocalDate': 'LocalDate', - 'LocalDateTime': 'LocalDateTime', - 'Date': 'LocalDateTime', - 'BigDecimal': 'BigDecimal', - 'UUID': 'UUID', + + JAVA_TYPES = { + StringType.name: "String", + BooleanType.name: "Boolean", + IntegerType.name: "Integer", + FloatType.name: "Float", + DateType.name: "LocalDate", + DateTimeType.name: "LocalDateTime", + TimeType.name: "LocalDateTime", + TimeDeltaType.name: "Duration" } - - def __init__(self, model, output_path: str = "./generated/entities", - package_name: str = "com.example.entity", - templates_path: str = "./templates"): - """ - Inicijalizuj generator - - Args: - model: B-UML Diagram model - output_path: Putanja gde će biti sačuvane generirane klase - package_name: Java package za generirane klase - templates_path: Putanja do Jinja2 template fajlova - """ - super().__init__(model) - self.output_path = Path(output_path) - self.package_name = package_name - self.templates_path = Path(templates_path) - self.output_path.mkdir(parents=True, exist_ok=True) - - # Inicijalizuj Jinja2 okruženje - self.jinja_env = Environment( - loader=FileSystemLoader(str(self.templates_path)), - trim_blocks=True, - lstrip_blocks=True - ) - - # Mapiranje B-UML klasa sa njihovim asocijacijama - self.associations_by_class: Dict[str, List[Association]] = {} - self.enumerations: Dict[str, Enumeration] = {} - - self._build_association_map() - self._build_enumerations_map() - - def _build_association_map(self): - """Gradi mapu asocijacija za svaku klasu""" - if not hasattr(self.model, 'classes') or self.model.classes is None: - return - - for cls in self.model.classes: - self.associations_by_class[cls.name] = [] - - for cls in self.model.classes: - if hasattr(cls, 'association_ends') and cls.association_ends: - for assoc_end in cls.association_ends: - if hasattr(assoc_end, 'association'): - assoc = assoc_end.association - if assoc not in self.associations_by_class[cls.name]: - self.associations_by_class[cls.name].append(assoc) - - def _build_enumerations_map(self): - """Gradi mapu enumeracija""" - if not hasattr(self.model, 'enumerations') or self.model.enumerations is None: - return - - for enum in self.model.enumerations: - self.enumerations[enum.name] = enum - - def generate(self) -> List[str]: - """ - Generiši sve Spring Entity klase i enumeracije - - Returns: - Lista putanja generianih datoteka - """ - generated_files = [] - - # Generiši enumeracije - if hasattr(self.model, 'enumerations') and self.model.enumerations: - for enum in self.model.enumerations: - try: - file_path = self._generate_enumeration_class(enum) - generated_files.append(file_path) - except Exception as e: - print(f"Greška pri generisanju enumeracije {enum.name}: {e}") - - # Generiši entitete - if not hasattr(self.model, 'classes') or self.model.classes is None: - return generated_files - - for cls in self.model.classes: - try: - file_path = self._generate_entity_class(cls) - generated_files.append(file_path) - except Exception as e: - print(f"Greška pri generisanju klase {cls.name}: {e}") - - return generated_files - - def _generate_enumeration_class(self, enum: Enumeration) -> str: - """Generiši Java enumeraciju""" - enum_values = [] - - if hasattr(enum, 'literals') and enum.literals: - for literal in enum.literals: - enum_values.append(literal.name if hasattr(literal, 'name') else str(literal)) - - enum_code = f"""package {self.package_name}; - -/** - * Enumeracija {enum.name} - * - * Generirano automatski od BESSER B-UML modela - * - * @author Spring Entity Generator - * @version 1.0 - */ -public enum {enum.name} {{ -""" - - for i, value in enumerate(enum_values): - enum_code += f" {value}" - if i < len(enum_values) - 1: - enum_code += ",\n" - else: - enum_code += ";\n" - - enum_code += f""" - /** - * Pronađi enumeraciju po imenu - * - * @param value String vrednost - * @return {enum.name} enumeracija - */ - public static {enum.name} fromValue(String value) {{ - try {{ - return valueOf(value.toUpperCase()); - }} catch (IllegalArgumentException e) {{ - return null; - }} - }} -}} -""" - - # Napiši datoteku - file_path = self.output_path / f"{enum.name}.java" - with open(file_path, 'w', encoding='utf-8') as f: - f.write(enum_code) - - return str(file_path) - - def _generate_entity_class(self, buml_class: Class) -> str: - """Generiši jednu Spring Entity klasu""" + + def __init__(self, model: DomainModel, + output_dir: str = "./generated/entities", + package_name: str = "com.example.entities"): + super().__init__(model, output_dir) + + self.package_name: str = package_name + self.enumerations: set[Enumeration] = model.get_enumerations() + self.relation_owners: dict[str, str] = self._get_relation_owner_map(model) + + def generate(self): + model: DomainModel = self.model + assoc_map: defaultdict[str, List[BinaryAssociation]] = defaultdict(list) + + for assoc in model.associations: + end1, end2 = list(assoc.ends) + assoc_map[end1.type.name].append(assoc) + assoc_map[end2.type.name].append(assoc) + + for enum in model.get_enumerations(): + self._generate_enum_file(enum) - # Pripremi podatke za template + for cls in model.classes_sorted_by_inheritance(): + self._generate_class_file(cls, assoc_map[cls.name]) + + def _generate_class_file(self, cls: Class, assocs_for_class: List[BinaryAssociation]): + file_path = self.build_generation_path(file_name=f"{cls.name.capitalize()}.java") + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path)) + entity_template = env.get_template("entity.java.j2") + + imports: set[str] = self._get_default_imports().union(self._get_specific_imports_for_class(cls)) + context = { - 'class_name': buml_class.name, - 'package_name': self.package_name, - 'table_name': self._to_snake_case(buml_class.name), - 'parent': self._get_parent_class(buml_class), - 'is_abstract': self._is_abstract_class(buml_class), - 'description': self._get_description(buml_class), - 'imports': self._generate_imports(buml_class), - 'attributes': self._prepare_attributes(buml_class), - 'relations': self._prepare_relations(buml_class), - 'unique_constraints': self._prepare_unique_constraints(buml_class), - 'constructor_params': self._generate_constructor_params(buml_class), - 'constructor_params_list': self._generate_constructor_params_list(buml_class), - 'constructor_assignments': self._generate_constructor_assignments(buml_class), - 'getters_setters': self._prepare_getters_setters(buml_class), + "cls": cls, + "package_name": self.package_name, + "imports": sorted(list(imports)), + "is_abstract": cls.is_abstract, + "table_name": self._pluralize(cls.name.lower()), + "parent": cls.parents().pop().name if cls.parents() else None, + "attributes": sorted( + self._prepare_attributes(cls), + key=lambda a: (not a["is_id"], not a["is_enum"], not a["is_list"], a["name"]) + ), + "relations": self._prepare_relations(cls, assocs_for_class), } - - # Učitaj i primeni template - template = self.jinja_env.get_template('entity_template.j2') - entity_code = template.render(**context) - - # Napiši datoteku - file_path = self.output_path / f"{buml_class.name}.java" - with open(file_path, 'w', encoding='utf-8') as f: - f.write(entity_code) - - return str(file_path) - - def _is_abstract_class(self, buml_class: Class) -> bool: - """Proverite da li je klasa apstraktna""" - return hasattr(buml_class, 'is_abstract') and buml_class.is_abstract - - def _get_parent_class(self, buml_class: Class) -> Optional[str]: - """Pronađi parent klasu ako postoji generalizacija""" - if hasattr(buml_class, 'parents') and buml_class.parents: - parents = buml_class.parents - if parents: - parent = list(parents)[0] - return parent.name - return None - - def _get_description(self, buml_class: Class) -> Optional[str]: - """Preuzmi opis klase""" - if hasattr(buml_class, 'metadata') and buml_class.metadata: - if hasattr(buml_class.metadata, 'description'): - return buml_class.metadata.description - return None - - def _get_visibility(self, element) -> str: - """Preuzmi visibility modifier""" - visibility = "private" # Default - - if hasattr(element, 'visibility'): - vis = element.visibility - if vis is None: - visibility = "public" - elif isinstance(vis, str): - vis_lower = vis.lower() - if 'public' in vis_lower: - visibility = "public" - elif 'protected' in vis_lower: - visibility = "protected" - elif 'private' in vis_lower: - visibility = "private" - else: - # Ako je VisibilityKind enum - try: - if hasattr(vis, 'name'): - vis_name = vis.name.lower() - if 'public' in vis_name: - visibility = "public" - elif 'protected' in vis_name: - visibility = "protected" - elif 'private' in vis_name: - visibility = "private" - except: - pass - - return visibility - - def _generate_imports(self, buml_class: Class) -> List[str]: - """Generiši potrebne Java importe""" - imports = set([ - "jakarta.persistence.Entity", - "jakarta.persistence.Table", - "jakarta.persistence.Id", - "jakarta.persistence.GeneratedValue", - "jakarta.persistence.GenerationType", - "jakarta.persistence.Column", - "jakarta.persistence.OneToOne", - "jakarta.persistence.OneToMany", - "jakarta.persistence.ManyToOne", - "jakarta.persistence.ManyToMany", - "jakarta.persistence.JoinColumn", - "jakarta.persistence.JoinTable", - "jakarta.persistence.FetchType", - "jakarta.persistence.CascadeType", - "jakarta.persistence.Enumerated", - "jakarta.persistence.EnumType", - "jakarta.persistence.UniqueConstraint", - "java.time.LocalDate", - "java.time.LocalDateTime", - "java.util.HashSet", - "java.util.Set", - "java.util.Objects", - ]) - - # Dodaj inheritance import ako je potrebna - if self._is_abstract_class(buml_class): - imports.add("jakarta.persistence.Inheritance") - imports.add("jakarta.persistence.InheritanceType") - - # Dodaj importe na osnovu atributa - if buml_class.attributes: - for attr in buml_class.attributes: - if self._is_readonly_attribute(attr): - imports.add("org.hibernate.annotations.Immutable") - - if self._is_enum_attribute(attr): - enum_type = self._get_enum_name(attr.type) - imports.add(f"{self.package_name}.{enum_type}") - elif hasattr(attr.type, 'name'): - type_name = attr.type.name - if 'BigDecimal' in type_name: - imports.add("java.math.BigDecimal") - if 'UUID' in type_name: - imports.add("java.util.UUID") - - return sorted(list(imports)) - - def _is_readonly_attribute(self, attr: Property) -> bool: - """Proverite da li je atribut readonly""" - return hasattr(attr, 'is_read_only') and attr.is_read_only - - def _is_derived_attribute(self, attr: Property) -> bool: - """Proverite da li je atribut izvedena vrednost""" - return hasattr(attr, 'is_derived') and attr.is_derived - - def _is_enum_attribute(self, attr: Property) -> bool: - """Proverite da li je atribut enumeracija""" - if hasattr(attr, 'type') and attr.type: - type_name = attr.type.name if hasattr(attr.type, 'name') else str(attr.type) - return type_name in self.enumerations - return False - - def _get_enum_name(self, attr_type) -> str: - """Preuzmi naziv enumeracije""" - if hasattr(attr_type, 'name'): - return attr_type.name - return str(attr_type) - - def _prepare_attributes(self, buml_class: Class) -> List[Dict]: - """Pripremi podatke za atribute""" - attributes = [] - - if not buml_class.attributes: - return attributes - - for attr in buml_class.attributes: - if self._is_association_end(attr): - continue - - # Proverite da li je ovo ID polje - is_id = hasattr(attr, 'is_id') and attr.is_id - - # Visibility - visibility = self._get_visibility(attr) - - # Readonly - is_readonly = self._is_readonly_attribute(attr) - - # Derived - is_derived = self._is_derived_attribute(attr) - - # Enumeracija - is_enum = self._is_enum_attribute(attr) - - java_type = self._map_type_to_java(attr.type) - column_name = self._to_snake_case(attr.name) - - # Nullable - nullable = True - if hasattr(attr, 'lower') and attr.lower == 1: - nullable = False - - # Length - length = None - if java_type == 'String': - length = 255 - - # Unique - unique = False - if 'email' in attr.name.lower() or 'username' in attr.name.lower(): - unique = True - - # Opis - description = None - if hasattr(attr, 'metadata') and attr.metadata: - if hasattr(attr.metadata, 'description'): - description = attr.metadata.description - - # Default vrednost - default_value = None - if hasattr(attr, 'default_value'): - default_value = attr.default_value - - # Updatable/Insertable - updatable = None - insertable = None - - if is_readonly: - updatable = False - - if is_id or is_derived: - insertable = False - - # Enum vrednosti - enum_values = None + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = entity_template.render(**context) + f.write(generated_code) + + def _prepare_attributes(self, cls: Class) -> List[object]: + attributes: List[object] = [] + + for attr in cls.attributes: + is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) + is_list: bool = attr.multiplicity.max != 1 + attr_type = None + if is_enum: - enum_name = self._get_enum_name(attr.type) - if enum_name in self.enumerations: - enum = self.enumerations[enum_name] - if hasattr(enum, 'literals'): - enum_values = [lit.name if hasattr(lit, 'name') else str(lit) for lit in enum.literals] - + attr_type = attr.type.name + else: + attr_type = self.JAVA_TYPES[attr.type.name] + if is_list: + attr_type = f"List<{attr_type}>" + attributes.append({ - 'name': attr.name, - 'java_type': java_type, - 'column_name': column_name, - 'visibility': visibility, - 'nullable': nullable, - 'unique': unique, - 'length': length, - 'is_id': is_id, - 'is_enum': is_enum, - 'is_readonly': is_readonly, - 'is_derived': is_derived, - 'description': description, - 'default_value': default_value, - 'updatable': updatable, - 'insertable': insertable, - 'enum_values': ", ".join(enum_values) if enum_values else None + "is_id": attr.is_id, + "column_name": self._to_snake_case(attr.name), + "is_optional": attr.is_optional, + "is_enum": is_enum, + "is_list": is_list, + "visibility": attr.visibility, + "type": attr_type, + "name": attr.name, + "default_value": f"\"{attr.default_value}\"" if attr.default_value and attr.type.name == "str" and not is_list else attr.default_value }) return attributes - def _prepare_relations(self, buml_class: Class) -> List[Dict]: - """Pripremi podatke za relacije""" - relations = [] - - if buml_class.name not in self.associations_by_class: - return relations - - for assoc in self.associations_by_class[buml_class.name]: - source_end = None - target_end = None - - if hasattr(assoc, 'ends') and assoc.ends: - for end in assoc.ends: - if hasattr(end, 'type') and end.type == buml_class: - source_end = end - else: - target_end = end - - if not source_end or not target_end: - continue - - relation_type = self._determine_relation_type(source_end, target_end) - target_class = target_end.type.name if hasattr(target_end, 'type') else "Unknown" - field_name = self._to_camel_case_first_lower(target_class) - plural_name = self._pluralize(field_name) - plural_name_capitalized = self._capitalize_first(plural_name) - - # Visibility - visibility = self._get_visibility(source_end) - - # Navigable - is_navigable = hasattr(source_end, 'is_navigable') and source_end.is_navigable - - # Owning side - is_owning_side = relation_type in ["ManyToOne", "ManyToMany"] - - # Cascade types - cascade_types = ["PERSIST", "MERGE"] - if relation_type == "OneToMany": - cascade_types = ["ALL"] - - # Fetch type - fetch_type = "LAZY" - - # Optional - is_optional = True - if relation_type in ["ManyToOne"]: - if hasattr(target_end, 'lower') and target_end.lower == 1: - is_optional = False - - # Opis - description = None - if hasattr(source_end, 'metadata') and source_end.metadata: - if hasattr(source_end.metadata, 'description'): - description = source_end.metadata.description - - side_description = "OWNING SIDE" if is_owning_side else "INVERSE SIDE" - cascade_types_str = ", ".join(cascade_types) - - relation_data = { - 'type': relation_type, - 'target_class': target_class, - 'field_name': field_name, - 'plural_name': plural_name, - 'plural_name_capitalized': plural_name_capitalized, - 'join_column_name': self._to_snake_case(field_name) + '_id', - 'mapped_by': self._to_camel_case_first_lower(buml_class.name), - 'source_class': buml_class.name, - 'join_table_name': f"{self._to_snake_case(buml_class.name)}_{self._to_snake_case(target_class)}", - 'source_join_column': f"{self._to_snake_case(buml_class.name)}_id", - 'target_join_column': f"{self._to_snake_case(target_class)}_id", - 'visibility': visibility, - 'is_owning_side': is_owning_side, - 'is_navigable': is_navigable, - 'cascade_types': cascade_types, - 'cascade_types_str': cascade_types_str, - 'fetch_type': fetch_type, - 'is_optional': is_optional, - 'side_description': side_description, - 'description': description - } - - relations.append(relation_data) - - return relations - - def _prepare_unique_constraints(self, buml_class: Class) -> List[Dict]: - """Pripremi podatke za unique constraints""" - constraints = [] - - if not buml_class.attributes: - return constraints - - unique_attrs = [] - for attr in buml_class.attributes: - if self._is_association_end(attr): - continue - - if 'email' in attr.name.lower() or 'username' in attr.name.lower(): - unique_attrs.append(attr.name) - - if unique_attrs: - constraint = { - 'name': f"uk_{self._to_snake_case(buml_class.name)}_{'_'.join([self._to_snake_case(a) for a in unique_attrs])}", - 'columns': ', '.join([f'"{self._to_snake_case(a)}"' for a in unique_attrs]) - } - constraints.append(constraint) - - return constraints - - def _generate_constructor_params(self, buml_class: Class) -> str: - """Generiši parametre za konstruktor""" - all_attrs = self._get_all_attributes(buml_class) - - if not all_attrs: - return "" - - params = [] - for attr in all_attrs: - java_type = self._map_type_to_java(attr.type) - attr_name = attr.name if hasattr(attr, 'name') else "unknown" - params.append(f"{java_type} {attr_name}") - - return ", ".join(params) - - def _generate_constructor_params_list(self, buml_class: Class) -> List[Dict]: - """Generiši listu parametara sa tipom""" - all_attrs = self._get_all_attributes(buml_class) - params = [] - - for attr in all_attrs: - java_type = self._map_type_to_java(attr.type) - attr_name = attr.name if hasattr(attr, 'name') else "unknown" - params.append({ - 'name': attr_name, - 'type': java_type - }) - - return params - - def _generate_constructor_assignments(self, buml_class: Class) -> List[str]: - """Generiši dodele vrednosti u konstruktoru""" - all_attrs = self._get_all_attributes(buml_class) - assignments = [] - - for attr in all_attrs: - attr_name = attr.name if hasattr(attr, 'name') else "unknown" - assignments.append(f"this.{attr_name} = {attr_name};") - - return assignments - - def _get_all_attributes(self, buml_class: Class) -> List: - """Preuzmi sve atribute (i obične i relacijske)""" - all_attrs = [] - - if buml_class.attributes: - for attr in buml_class.attributes: - if not self._is_association_end(attr): - all_attrs.append(attr) - - if buml_class.name in self.associations_by_class: - for assoc in self.associations_by_class[buml_class.name]: - for end in assoc.ends: - if hasattr(end, 'type') and end.type != buml_class: - all_attrs.append(end) - break - - return all_attrs - - def _prepare_getters_setters(self, buml_class: Class) -> List[Dict]: - """Pripremi podatke za getter i setter metode""" - getters_setters = [] - - if not buml_class.attributes: - return getters_setters - - for attr in buml_class.attributes: - if self._is_association_end(attr): + def _prepare_relations(self, cls: Class, assocs_for_class: List[BinaryAssociation]) -> List[object]: + relations: List[object] = [] + + for assoc in assocs_for_class: + ends: List[Property] = list(assoc.ends) + end1, end2 = ends + + if end1.type.name == cls.name and end2.is_navigable: + source: Property = end1 + target: Property = end2 + elif end2.type.name == cls.name and end1.is_navigable: + source: Property = end2 + target: Property = end1 + else: continue - - java_type = self._map_type_to_java(attr.type) - attr_name = attr.name if hasattr(attr, 'name') else "unknown" - method_name = self._capitalize_first(attr_name) - visibility = self._get_visibility(attr) - - getters_setters.append({ - 'java_type': java_type, - 'attribute_name': attr_name, - 'method_name': method_name, - 'visibility': visibility + + bidirectional: bool = source.is_navigable and target.is_navigable + source_many = source.multiplicity.max != 1 + target_many = target.multiplicity.max != 1 + + relation: str = None + mapped_by: str = None + owning: bool = True + + if not source_many and not target_many: + relation = "OneToOne" + if bidirectional and self.relation_owners[assoc.name] != source.type.name: + owning = False + mapped_by = source.name + elif not source_many and target_many: + relation = "OneToMany" + if bidirectional: + owning = False + mapped_by = source.name + elif source_many and not target_many: + relation = "ManyToOne" + elif source_many and target_many: + relation = "ManyToMany" + if bidirectional and self.relation_owners[assoc.name] != source.type.name: + owning = False + mapped_by = source.name + + relations.append({ + "assoc": assoc.name, + "source_property": source.name, + "target_property": target.name, + "source_cls": source.type.name, + "target_cls": target.type.name, + "relation": relation, + "mapped_by": mapped_by, + "join_column": f"{self._to_snake_case(target.type.name)}_id", + "owning": owning, + "is_list": target_many, + "type": f"List<{target.type.name}>" if target_many else target.type.name, + "to_snake_case": self._to_snake_case }) - - return getters_setters + + return relations - def _is_association_end(self, attr: Property) -> bool: - """Proverite da li je atribut asocijacijski kraj""" - if hasattr(attr, 'type') and attr.type: - type_name = attr.type.name if hasattr(attr.type, 'name') else str(attr.type) - return type_name[0].isupper() and type_name not in self.JAVA_TYPE_MAPPING - return False + def _get_relation_owner_map(self, model: DomainModel) -> dict[str, str]: + ret: dict[str, str] = {} + + for assoc in model.associations: + end1, end2 = list(assoc.ends) + if (end1.multiplicity != 1 and end2.multiplicity != 1) or (end1.multiplicity == 1 and end2.multiplicity == 1) and end1.is_navigable and end2.is_navigable: + ret[assoc.name] = end1.type.name + + return ret + + def _generate_enum_file(self, enum: Enumeration): + file_path = self.build_generation_path(file_name=f"{enum.name.capitalize()}.java") + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path)) + entity_template = env.get_template("enum.java.j2") + + context = { + "package_name": self.package_name, + "name": enum.name.capitalize(), + "literals": enum.literals + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = entity_template.render(**context) + f.write(generated_code) + + def _get_default_imports(self) -> set[str]: + return set([ + "javax.persistence.Entity", + "javax.persistence.Table", + "javax.persistence.Id", + "javax.persistence.GeneratedValue", + "javax.persistence.GenerationType", + "javax.persistence.Column", + ]) - def _map_type_to_java(self, buml_type) -> str: - """Mapira B-UML tip na Java tip""" - if buml_type is None: - return "Object" - - type_name = buml_type.name if hasattr(buml_type, 'name') else str(buml_type) - - if type_name in self.enumerations: - return type_name - - return self.JAVA_TYPE_MAPPING.get(type_name, type_name) + def _get_specific_imports_for_class(self, cls: Class) -> set[str]: + imports: set[str] = set() + + if cls.is_abstract: + imports.add("javax.persistence.MappedSuperclass") + + if any(any(attr.type.name == enum.name for enum in self.enumerations) for attr in cls.attributes): + imports.add("javax.persistence.Enumerated") + imports.add("javax.persistence.EnumType") + + for attr in cls.attributes: + if attr.type.name == DateType.name: + imports.add("java.time.LocalDate") + elif attr.type.name in [DateTimeType.name, TimeType.name]: + imports.add("java.time.LocalDateTime") + elif attr.type.name == TimeDeltaType.name: + imports.add("java.time.Duration") + + if attr.multiplicity.max != 1: + imports.add("java.util.List") + imports.add("java.util.ArrayList") + imports.add("java.util.Arrays") + + return imports - def _determine_relation_type(self, source_end, target_end) -> str: - """Odredi tip relacije na osnovu multipliciteta""" - source_mult = self._get_multiplicity(source_end) - target_mult = self._get_multiplicity(target_end) - - if target_mult in ["0..1", "1"]: - if source_mult in ["0..*", "*"]: - return "ManyToOne" - return "OneToOne" - elif target_mult in ["0..*", "*"]: - if source_mult in ["0..*", "*"]: - return "ManyToMany" - return "OneToMany" + @staticmethod + def _pluralize(name: str) -> str: + if name.endswith("y"): + return name[:-1] + "ies" + elif name.endswith(("s", "x", "z", "ch", "sh")): + return name + "es" + else: + return name + "s" - return "ManyToOne" - - def _get_multiplicity(self, end) -> str: - """Pronađi multipliciteta asocijacionog kraja""" - if hasattr(end, 'multiplicity') and end.multiplicity: - mult = end.multiplicity - if hasattr(mult, 'lower') and hasattr(mult, 'upper'): - if mult.upper == -1: - return f"{mult.lower}..*" - return f"{mult.lower}..{mult.upper}" - return "0..1" - @staticmethod def _to_snake_case(name: str) -> str: - """Konvertuje CamelCase u snake_case""" s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() - - @staticmethod - def _to_camel_case_first_lower(name: str) -> str: - """Konvertuje CamelCase u camelCase""" - if not name: - return name - return name[0].lower() + name[1:] - - @staticmethod - def _capitalize_first(name: str) -> str: - """Velikim baš prvi karakter""" - if not name: - return name - return name[0].upper() + name[1:] - - @staticmethod - def _pluralize(name: str) -> str: - """Pluralizuje ime""" - if name.endswith('y'): - return name[:-1] + 'ies' - elif name.endswith(('s', 'x', 'z', 'ch', 'sh')): - return name + 'es' - else: - return name + 's' \ No newline at end of file diff --git a/besser/generators/spring/templates/entity.j2 b/besser/generators/spring/templates/entity.j2 deleted file mode 100644 index 298576062..000000000 --- a/besser/generators/spring/templates/entity.j2 +++ /dev/null @@ -1,90 +0,0 @@ -package {{ package_name }}; - -{% for import in imports -%} -import {{ import }}; -{% endfor -%} - -{% if is_abstract %} -@MappedSuperclass -{% else %} -@Entity -@Table(name = "{{ table_name }}") -{% endif -%} -public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent %} extends {{parent}}{% endif %} { - {% for attribute in attributes -%} - {% if attribute.is_id %} - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - {%- endif %} - {% if attribute.is_enum -%} - @Enumerated(EnumType.STRING) - {% else -%} - @Column(name = "{{ attribute.column_name }}"{% if not attribute.is_optional %}, nullable = false{% endif %}) - {% endif -%} - {% if not attribute.is_list %} - {{- attribute.visibility }} {{ attribute.type }} {{ attribute.name }}{% if attribute.default_value %} = {{ attribute.default_value }}{% endif %}; - {%- else -%} - {{- attribute.visibility }} {{ attribute.type }} {{ attribute.name }} = new ArrayList<>({% if attribute.default_value %}Arrays.asList({{ attribute.default_value }}){% endif %}); - {%- endif %} - {% endfor -%} - - {% for rel in relations -%} - - {% if rel.relation == "OneToOne" %} - @OneToOne{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} - {% if rel.owning -%} - @JoinColumn(name = "{{ rel.join_column }}") - {%- endif %} - {% endif -%} - - {% if rel.relation == "OneToMany" %} - @OneToMany{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} - {% if rel.owning -%} - @JoinColumn(name = "{{ rel.join_column }}") - {%- endif %} - {% endif -%} - - {% if rel.relation == "ManyToOne" %} - @ManyToOne - @JoinColumn(name = "{{ rel.join_column }}") - {% endif -%} - - {% if rel.relation == "ManyToMany" %} - @ManyToMany{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} - @JoinTable(name = "{{ rel.assoc }}", - joinColumns = @JoinColumn(name = "{{ rel.to_snake_case(rel.source_cls) }}_id"), - inverseJoinColumns = @JoinColumn(name = "{{ rel.to_snake_case(rel.target_cls) }}_id")) - {% endif -%} - - {%- if rel.is_list -%} - private {{ rel.type }} {{ rel.target_property }} = new ArrayList<>(); - {% else -%} - private {{ rel.type }} {{ rel.target_property }}; - {% endif -%} - {%- endfor -%} - - {% if not is_abstract %} - public {{ cls.name }}() { } - - {% if attributes -%} - public {{ cls.name }}( - {%- for attribute in attributes -%} - {{ attribute.type }} {{ attribute.name }}{% if not loop.last %}, {% endif %} - {%- endfor %}) { - {%- for attribute in attributes %} - this.{{ attribute.name }} = {{ attribute.name }}; - {%- endfor %} - } - {%- endif %} - {% endif -%} - - {% for attribute in attributes %} - public {{ attribute.type }} get{{ attribute.name | capitalize }}() { - return this.{{ attribute.name }}; - } - - public void set{{ attribute.name | capitalize }}({{ attribute.type }} {{ attribute.name }}) { - this.{{ attribute.name }} = {{ attribute.name }}; - } - {% endfor %} -} \ No newline at end of file diff --git a/besser/generators/spring/templates/entity.java.j2 b/besser/generators/spring/templates/entity.java.j2 index bd5b8634d..27716925f 100644 --- a/besser/generators/spring/templates/entity.java.j2 +++ b/besser/generators/spring/templates/entity.java.j2 @@ -1,61 +1,92 @@ package {{ package_name }}; -import jakarta.persistence.*; -import jakarta.validation.constraints.*; -import java.util.*; +{% for import in imports -%} +import {{ import }}; +{% endfor -%} +{% if is_abstract %} +@MappedSuperclass +{% else %} @Entity -@Table(name = "{{ class.name|lower }}") -public class {{ class.name }} { - +@Table(name = "{{ table_name }}") +{% endif -%} +public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent %} extends {{parent}}{% endif %} { + {% for attribute in attributes -%} + {% if attribute.is_id %} @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - -{% for attr in attributes %} - -{% if not attr.optional %} - @NotNull -{% endif %} - @Column(name = "{{ attr.column }}") - {{ attr.visibility }} {{ attr.type }} {{ attr.name }}; - -{% endfor %} - -{% for attr in enum_attributes %} - + {%- endif %} + {% if attribute.is_enum -%} @Enumerated(EnumType.STRING) - {{ attr.visibility }} {{ attr.type }} {{ attr.name }}; - -{% endfor %} - -{% for rel in relations %} - -{{ rel.annotation }} - {{ rel.visibility }} {{ rel.type }} {{ rel.name }}{% if rel.collection %} = new ArrayList<>(){% endif %}; - -{% endfor %} - - public {{ class.name }}() {} - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; + {% else -%} + @Column(name = "{{ attribute.column_name }}"{% if not attribute.is_optional %}, nullable = false{% endif %}) + {% endif -%} + {% if not attribute.is_list %} + {{- attribute.visibility }} {{ attribute.type }} {{ attribute.name }}{% if attribute.default_value %} = {{ attribute.default_value }}{% endif %}; + {%- else -%} + {{- attribute.visibility }} {{ attribute.type }} {{ attribute.name }} = new ArrayList<>({% if attribute.default_value %}Arrays.asList({{ attribute.default_value }}){% endif %}); + {%- endif %} + {% endfor -%} + + {% for rel in relations -%} + + {% if rel.relation == "OneToOne" %} + @OneToOne{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} + {% if rel.owning -%} + @JoinColumn(name = "{{ rel.join_column }}") + {%- endif %} + {% endif -%} + + {% if rel.relation == "OneToMany" %} + @OneToMany{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} + {% if rel.owning -%} + @JoinColumn(name = "{{ rel.join_column }}") + {%- endif %} + {% endif -%} + + {% if rel.relation == "ManyToOne" %} + @ManyToOne + @JoinColumn(name = "{{ rel.join_column }}") + {% endif -%} + + {% if rel.relation == "ManyToMany" %} + @ManyToMany{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} + {% if rel.owning -%} + @JoinTable(name = "{{ rel.to_snake_case(rel.assoc) }}", + joinColumns = @JoinColumn(name = "{{ rel.to_snake_case(rel.source_cls) }}_id"), + inverseJoinColumns = @JoinColumn(name = "{{ rel.to_snake_case(rel.target_cls) }}_id")) + {%- endif %} + {% endif -%} + + {%- if rel.is_list -%} + private {{ rel.type }} {{ rel.target_property }} = new ArrayList<>(); + {% else -%} + private {{ rel.type }} {{ rel.target_property }}; + {% endif -%} + {%- endfor -%} + + {% if not is_abstract %} + public {{ cls.name }}() { } + + {% if attributes -%} + public {{ cls.name }}( + {%- for attribute in attributes -%} + {{ attribute.type }} {{ attribute.name }}{% if not loop.last %}, {% endif %} + {%- endfor %}) { + {%- for attribute in attributes %} + this.{{ attribute.name }} = {{ attribute.name }}; + {%- endfor %} } + {%- endif %} + {% endif -%} -{% for field in fields %} - - public {{ field.type }} get{{ field.name|capitalize }}() { - return {{ field.name }}; + {% for attribute in attributes %} + public {{ attribute.type }} get{{ attribute.name | capitalize }}() { + return this.{{ attribute.name }}; } - public void set{{ field.name|capitalize }}({{ field.type }} {{ field.name }}) { - this.{{ field.name }} = {{ field.name }}; + public void set{{ attribute.name | capitalize }}({{ attribute.type }} {{ attribute.name }}) { + this.{{ attribute.name }} = {{ attribute.name }}; } - -{% endfor %} - + {% endfor %} } \ No newline at end of file From 677f6e9cea4b451a157df3aba468b4cf56529e50 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Thu, 12 Mar 2026 12:04:08 +0100 Subject: [PATCH 06/16] backend --- .../spring/backend/.idea/.gitignore | 10 + besser/generators/spring/backend/.mvn/mvnw | 316 ++++++++++++++++++ .../generators/spring/backend/.mvn/mvnw.cmd | 188 +++++++++++ .../.mvn/wrapper/maven-wrapper.properties | 3 + .../java/com/transformers/Transformers.java | 13 + .../com/transformers/entities/Computer.java | 53 +++ .../java/com/transformers/entities/Hand.java | 5 + .../java/com/transformers/entities/Role.java | 5 + .../java/com/transformers/entities/Thing.java | 14 + .../java/com/transformers/entities/User.java | 83 +++++ .../src/main/resources/application.properties | 11 + .../com/transformers/TransformersTests.java | 13 + .../target/classes/application.properties | 11 + .../com/transformers/Transformers.class | Bin 0 -> 724 bytes .../com/transformers/entities/Computer.class | Bin 0 -> 1626 bytes .../com/transformers/entities/Hand.class | Bin 0 -> 1120 bytes .../com/transformers/entities/Role.class | Bin 0 -> 1120 bytes .../com/transformers/entities/Thing.class | Bin 0 -> 373 bytes .../com/transformers/entities/User.class | Bin 0 -> 2535 bytes besser/generators/spring/proba.py | 25 +- .../spring/spring_backend_generator.py | 136 ++++++++ .../spring/spring_entity_generator.py | 47 ++- .../templates/application.properties.j2 | 11 + .../generators/spring/templates/main.java.j2 | 13 + .../templates/maven-wrapper.properties.j2 | 3 + .../generators/spring/templates/mvnw.cmd.j2 | 188 +++++++++++ besser/generators/spring/templates/mvnw.j2 | 316 ++++++++++++++++++ besser/generators/spring/templates/pom.xml.j2 | 54 +++ .../generators/spring/templates/test.java.j2 | 13 + 29 files changed, 1510 insertions(+), 21 deletions(-) create mode 100644 besser/generators/spring/backend/.idea/.gitignore create mode 100644 besser/generators/spring/backend/.mvn/mvnw create mode 100644 besser/generators/spring/backend/.mvn/mvnw.cmd create mode 100644 besser/generators/spring/backend/.mvn/wrapper/maven-wrapper.properties create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entities/Computer.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entities/Hand.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entities/Role.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entities/Thing.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entities/User.java create mode 100644 besser/generators/spring/backend/src/main/resources/application.properties create mode 100644 besser/generators/spring/backend/src/test/java/com/transformers/TransformersTests.java create mode 100644 besser/generators/spring/backend/target/classes/application.properties create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/Transformers.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entities/Computer.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entities/Hand.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entities/Role.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entities/Thing.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entities/User.class create mode 100644 besser/generators/spring/spring_backend_generator.py create mode 100644 besser/generators/spring/templates/application.properties.j2 create mode 100644 besser/generators/spring/templates/main.java.j2 create mode 100644 besser/generators/spring/templates/maven-wrapper.properties.j2 create mode 100644 besser/generators/spring/templates/mvnw.cmd.j2 create mode 100644 besser/generators/spring/templates/mvnw.j2 create mode 100644 besser/generators/spring/templates/pom.xml.j2 create mode 100644 besser/generators/spring/templates/test.java.j2 diff --git a/besser/generators/spring/backend/.idea/.gitignore b/besser/generators/spring/backend/.idea/.gitignore new file mode 100644 index 000000000..ab1f4164e --- /dev/null +++ b/besser/generators/spring/backend/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/besser/generators/spring/backend/.mvn/mvnw b/besser/generators/spring/backend/.mvn/mvnw new file mode 100644 index 000000000..3933ce40b --- /dev/null +++ b/besser/generators/spring/backend/.mvn/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" \ No newline at end of file diff --git a/besser/generators/spring/backend/.mvn/mvnw.cmd b/besser/generators/spring/backend/.mvn/mvnw.cmd new file mode 100644 index 000000000..8e8900d34 --- /dev/null +++ b/besser/generators/spring/backend/.mvn/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% \ No newline at end of file diff --git a/besser/generators/spring/backend/.mvn/wrapper/maven-wrapper.properties b/besser/generators/spring/backend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..da4c7fc43 --- /dev/null +++ b/besser/generators/spring/backend/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java b/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java new file mode 100644 index 000000000..4895c0f5f --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java @@ -0,0 +1,13 @@ +package com.transformers; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Transformers { + + public static void main(String[] args) { + SpringApplication.run(Transformers.class, args); + } + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Computer.java b/besser/generators/spring/backend/src/main/java/com/transformers/entities/Computer.java new file mode 100644 index 000000000..28c83aaf6 --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entities/Computer.java @@ -0,0 +1,53 @@ +package com.transformers.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Entity +@Table(name = "computers") +public class Computer extends Thing { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + public Integer id; + + @Column(name = "model", nullable = false) + public String model; + + @ManyToMany(mappedBy = "computers") + + private List owners = new ArrayList<>(); + + public Computer() { } + + public Computer(Integer id, String model) { + this.id = id; + this.model = model; + } + + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getModel() { + return this.model; + } + + public void setModel(String model) { + this.model = model; + } + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Hand.java b/besser/generators/spring/backend/src/main/java/com/transformers/entities/Hand.java new file mode 100644 index 000000000..bbf863dd6 --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entities/Hand.java @@ -0,0 +1,5 @@ +package com.transformers.entities; + +public enum Hand { + RIGHT, LEFT +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Role.java b/besser/generators/spring/backend/src/main/java/com/transformers/entities/Role.java new file mode 100644 index 000000000..33f893173 --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entities/Role.java @@ -0,0 +1,5 @@ +package com.transformers.entities; + +public enum Role { + ADMIN, USER +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Thing.java b/besser/generators/spring/backend/src/main/java/com/transformers/entities/Thing.java new file mode 100644 index 000000000..52d41f587 --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entities/Thing.java @@ -0,0 +1,14 @@ +package com.transformers.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Table; + +@MappedSuperclass +public abstract class Thing { + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/User.java b/besser/generators/spring/backend/src/main/java/com/transformers/entities/User.java new file mode 100644 index 000000000..c3a23d202 --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entities/User.java @@ -0,0 +1,83 @@ +package com.transformers.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + public Integer id; + + @Enumerated(EnumType.STRING) + private Role role; + + @Column(name = "email") + public String email; + + @Column(name = "first_name") + public String firstName = "John"; + + @ManyToMany + @JoinTable(name = "many_to_many", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "computer_id")) + private List computers = new ArrayList<>(); + + public User() { } + + public User(Integer id, Role role, String email, String firstName) { + this.id = id; + this.role = role; + this.email = email; + this.firstName = firstName; + } + + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Role getRole() { + return this.role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFirstname() { + return this.firstName; + } + + public void setFirstname(String firstName) { + this.firstName = firstName; + } + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/resources/application.properties b/besser/generators/spring/backend/src/main/resources/application.properties new file mode 100644 index 000000000..62baa2e8c --- /dev/null +++ b/besser/generators/spring/backend/src/main/resources/application.properties @@ -0,0 +1,11 @@ +spring.application.name=Transformers + +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update + +spring.h2.console.enabled=true \ No newline at end of file diff --git a/besser/generators/spring/backend/src/test/java/com/transformers/TransformersTests.java b/besser/generators/spring/backend/src/test/java/com/transformers/TransformersTests.java new file mode 100644 index 000000000..c0538266b --- /dev/null +++ b/besser/generators/spring/backend/src/test/java/com/transformers/TransformersTests.java @@ -0,0 +1,13 @@ +package com.transformers; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class TransformersTests { + + @Test + void contextLoads() { + } + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/target/classes/application.properties b/besser/generators/spring/backend/target/classes/application.properties new file mode 100644 index 000000000..62baa2e8c --- /dev/null +++ b/besser/generators/spring/backend/target/classes/application.properties @@ -0,0 +1,11 @@ +spring.application.name=Transformers + +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update + +spring.h2.console.enabled=true \ No newline at end of file diff --git a/besser/generators/spring/backend/target/classes/com/transformers/Transformers.class b/besser/generators/spring/backend/target/classes/com/transformers/Transformers.class new file mode 100644 index 0000000000000000000000000000000000000000..ec08e47ebf1eb5cbce3ae25c283b83baad193510 GIT binary patch literal 724 zcmaKqO;6)65QblumXuPy%JQ{<5Vz6;zH#X;5>(<8KD0&M!=9Yr218<3j>GQX+7oCG z`~ZFw+HpW4hl)s<(ad;W+mGkv_1`mq@7QgjhM55MHX4{^SiTbvf~P_!{4%QGh1HZfr8{tkC(`*i71-Usr+_T9^;eZesz93_W8Lo=>dO$=Hfa{xEyzaQ@Z%VRROEU8S0yuaUaWr*PE(GJcJhzo1Ea?(DXfT6 zS=oZpzT9W156q2ZSPGSv7ey9Hd+paUbV3u0bR?|u@q4oFZdJ~(7k+wmFPjNPS6J^( zJMoC>!X_lNI;BkX&!xLHH&?=gas-y4H8h2d<(cwFTd8tB^o$s`eiYiNOpa8p{9Uy+ zuB1uL$5RoN@I~Pau__tK_oq0eSE;b-V;=#HhyQ9|en7}BQN+nJ@{Qgj*x$0eOxBk{ zEh}W(Zwt`DDn)bnf;AHH%}1pe(Ch@gC(Ql)==ZY-nw|jj6^$m=u|fGw`Wr-+Tn$^L M{fce!e%l@3KPSbyvH$=8 literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entities/Computer.class b/besser/generators/spring/backend/target/classes/com/transformers/entities/Computer.class new file mode 100644 index 0000000000000000000000000000000000000000..d2a1b3241aa9b6e17ce147c8f9e611018f2090c5 GIT binary patch literal 1626 zcmah}ZEw?77(I60ni6P%Hc(#H!V07TYAAaF$|^y}rbsQCXov*gF3nx)VP8k~HBNxSVvA*?n;&pd?GjLFS;v3(tIViQ3dNoWqIRA%wue{Xyf%i0y)$WF$*h$n- zs^b(IHJonX45l1hKbdR|3=%EUt5^ArDVpr0BOS!MmmQ2HVR-l^Cs zZ%8ZD)@ECKoj$`B$6o!a=~-Fb@2McwgiR!@Asy}hPXzV4yTdG^^Yil;-A?zlgDYb- z)}tT|4Y|ukg_;s34FW~$B{mWc>RbN6D4WK-TgfxdS|YW3vT<_hUnMNt8hIDS%5EyN z+l^$Ev_dr;>i#pPRVQQCGS;iS&O$qw+VG8jk%n(|+!bR|`Dj}OJr(MpOgCI22zMX{dNw!2__+cle$f5`4tdNCk6G iEA;f>1W);>6rQ9`E`G??AG67jNqNZgBf8{--TVSAAyjAp literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entities/Hand.class b/besser/generators/spring/backend/target/classes/com/transformers/entities/Hand.class new file mode 100644 index 0000000000000000000000000000000000000000..982d36f0d148daacec2ba7d907ba778f07077059 GIT binary patch literal 1120 zcmah{?{Csj6g`iY6>1p_adSB57^sx#7?XX1Ei(&qOR6)1B#WOO(6KJ1O-k8k|0HFJ zTQr(|_Kz~&S1@9x(xkn;_nmX@k9WWS`1}pPHntRm7?#^^ujqT+>7Te>&-D65)A23e zGW*2??sTGHkTrxc!w`OR_~M|!uwwk5QxT+yhNvLPMs2?#0=G11h%qR+=5yn1t(64Rc6R0d3oLOomimH_rGuFWTJc7LQtIX4|i*Na2oxc?}D=%P{*JP;&-7 z%6rc3fq8Vou$7-+@7cWHuS^8$zGpez%47y15a_2h+`|&XOx1EMe}`dyQfkaR)SD56 zRcbjMK~&g`hE-%m>p=~DhHPG++B~KC1()~S4)q;3EXRB`=(S9*!CN*(%74kPPn#-3 z(s0|{ZgS5OGGi^-;XRT}-kb$P;-%@Ix}DeDBaBa5LKo|wT4dCmNz=)VxUrGrdL7H* zHbb;-bsg>xJkpi&dXv_o+P25D+nh)wCS09X_fCb<)!l*DHuo*jeN=4dy13}w5{)@U zYc1_Aie&}J(*6O7v{*Yj36q~gmT(WrTO}(VbqMtnX0H+dKpw**`pyJr65$u{7@+`P zCMy_fk`=CS`x;E(Lx14&6wyW~=qQl4MsJuwe=nf4IgkRe5?P6kl)~E5B^E!@@4u83 zJP}<4O3@IWCV+K%iveXLp^C+#l%gdaGJS!$@K>;7X$Yx)EDxd7Z*~?z2^(YD_L!FW sLagL{=@QEqkOJp{95~7qRxY55GDibasay~(OwQS-U#X>0m1nd{~Zqj8v+pe}Zjn0mBMw9mT-gnNqKi>WR+MW;D#=A;Z*fK;0Si zDDMTg2j<}^!+K$iy<_u!zd9B$e9v;a)$t5MAkfcfc!YU|NX>FAf0JQmTx!JJ)0p=~DhFn3P*gU2A1($c+4)vXASdMux=(SAmEpOQrsr)6sK5429 zsfOF;c9VOSkQr&o4)2j<>h3HUlKZBA=62q2k1#%M30}N>vb%L z+YB+o>N?yXc%&;8^d_xEt!FkYOt?0w?#(KtGu(mKHg_%2eN1fVin!>lGL1P+ zYb|ZfiDd=I(*6O-j95E536q~kj&M)OTOunS^%CkQOx+^!fjow1^o;~(65(g@9H9VT zAuAYaiWP5g{}xQ(Lx1246w!t#=qQr6OmCP$e=nf4IgkReGFgd^l;ZOIHRe9j@4u83 zybxUkO0f`LCV>@tO95pysfxv-l%gdaGJS#R@K>-C=@OE0B40u=?sgVM8LK1O#)y{v sLafwd=^6`HkOJq495~7i7O$X+GKT|Fsay~(LtTVMwXES4#X_k31h%s4gpg6diO2NQUU4FEB3Eo063>8~K!%x(EXOYcdq(0^ReA)c|_} zr-iL$aLV`>>uT-1)Fx0M?d5VyW+RZ?*|ioJWn}bYQ|H<(RbH_&$!wvjm2wpMpO}O# zd4Xy6kMB$%UD(DI`kta$+D^NQkp)hk8xB$H6?tOoTVrfcfvjQYv#eCF$^|8NoR)mh zrqJ?1?RI*-Xjm&M<^3#~V!-Q&NATr^Fzz#xEeCp+9}w>>bdWN}XJCkf7GvLIcF47Z NqZUUPbB+IwfnT3mS~&m! literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entities/User.class b/besser/generators/spring/backend/target/classes/com/transformers/entities/User.class new file mode 100644 index 0000000000000000000000000000000000000000..3f6242f2b9e404b71750950536a5848c90b71532 GIT binary patch literal 2535 zcmai#>rxa~5Xb+s>=nkKC`8ZzB8jkoqlqyZ2tv5IWI==tRhkdf07n?IJ44NdDjp?o zkyM4UDyhl?(%a&+6xWjXB^iFXAN|Wjc*rGG>~2O4&4$u(N%zH;u=0PFu3P6tkAdJ;GO3; zlpk2i4V}o(ClSPYOXq(Lva5ieBGsF^zvJl=O((F7*HRokqruNdgm7rzsG zENwXlt{p}`O+2Lu7Td+;&3xnz7Lq%ko+U#%=9=xE);t+0GL(w9>N##r;-!SypyW>I z?Z|AP>xgG@et3K79z^{jg@uS^y(1;oYf^fOLA)J{bEq5kyu3e>MqSEHDoOKQ{W8AI=2}zX zp|`(b`z)5Y-*N{ktT5!>vGFFIbH}WXAU^L);}V+%8W0ErBQ()IJ`GB7+Ka{Sk9|s7 z%ok~XDQ?bNA0Q@yN>^P-^fp5#0%F9Zk!5+T;*vF~(X{Q{@*;mvEjtoKN$%mHlySS3 zTas5$D)J5!cJ)Cxug08IQ-FJXzA|jZ9Gj7E7{`0u4WR=gobPi#s_-Sg!UKwp1(X?o zi!#@I%M#&hoG193RG7j;F8E|6M_rsB@JGdAPJFko9#}NSXO`n5dd%RlCYa*tYwo`x z3ALZ#+h1VJ;c0B_J3MRNK4%at1>`w?Po*lA49& zG{+ufIhHv}8b%3G#tgrhu{Qs$X1No~FNh|Xmn#CN~H?kB3 zg}`I*Knj_PE8>YB?6DG-*feKbr zmZSTn7qGL>4Hs;Nxt=}b)(8QAn|5>mn)^*=j%~X2KfHbq<^TWy literal 0 HcmV?d00001 diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index 438698018..6efee46ac 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -10,6 +10,7 @@ BinaryAssociation, DateTimeType, DomainModel, Class, FloatType, Generalization, IntegerType, Multiplicity, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral ) from besser.generators.spring.spring_entity_generator import SpringEntityGenerator +from besser.generators.spring.spring_backend_generator import SpringBackendGenerator def proba_jednostavna_klasa(): current_dir = Path(__file__).parent @@ -20,7 +21,8 @@ def proba_jednostavna_klasa(): hand = Enumeration(name="Hand", literals={EnumerationLiteral(name="LEFT"), EnumerationLiteral(name="RIGHT")}) user = Class(name="User") - user.add_attribute(Property(name="email", type=StringType, is_id=True, is_optional=True)) + user.add_attribute(Property(name="id", type=IntegerType, is_id=True)) + user.add_attribute(Property(name="email", type=StringType, is_optional=True)) user.add_attribute(Property(name="firstName", type=StringType, is_optional=True, default_value="John")) # user.add_attribute(Property(name="age", type=IntegerType, default_value=2)) user.add_attribute(Property(name="role", type=role, visibility="private")) @@ -31,18 +33,19 @@ def proba_jednostavna_klasa(): thing = Class(name="Thing", is_abstract=True) computer = Class(name="Computer") - # computer.add_attribute(Property(name="name", type=StringType)) + computer.add_attribute(Property(name="id", type=IntegerType, is_id=True)) + computer.add_attribute(Property(name="model", type=StringType)) gen1 = Generalization(general=thing, specific=computer) # assoc = BinaryAssociation(name="onetoone", ends={ # Property(name="computer", type=computer, multiplicity=Multiplicity(1, 1), is_navigable=True), - # Property(name="owner", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True) + # Property(name="owner", type=user, multiplicity=Multiplicity(1, 1), is_navigable=False) # }) - # assoc2 = BinaryAssociation(name="onetomany", ends={ + # assoc = BinaryAssociation(name="onetomany", ends={ # Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True), - # Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True) + # Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=False) # }) # assoc2 = BinaryAssociation(name="manytoone", ends={ @@ -57,10 +60,14 @@ def proba_jednostavna_klasa(): model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}, associations={assoc}) - generator = SpringEntityGenerator( - model, - output_dir=output_dir, - package_name="spring.entities" + # generator = SpringEntityGenerator( + # model, + # output_dir=output_dir, + # package_name="spring.entities" + # ) + + generator = SpringBackendGenerator( + model, "3.5.11", java_version="17", app_name="Transformers", output_dir=current_dir / "backend", package_name="com.transformers" ) files = generator.generate() diff --git a/besser/generators/spring/spring_backend_generator.py b/besser/generators/spring/spring_backend_generator.py new file mode 100644 index 000000000..5b3a9e4f8 --- /dev/null +++ b/besser/generators/spring/spring_backend_generator.py @@ -0,0 +1,136 @@ +import os +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader + +from besser.BUML.metamodel.structural.structural import DomainModel +from besser.generators.generator_interface import GeneratorInterface +from besser.generators.spring.spring_entity_generator import SpringEntityGenerator + +class SpringBackendGenerator(GeneratorInterface): + + def __init__(self, + model: DomainModel, + spring_boot_version: str, + java_version: str, + app_name: str, + output_dir: str, + package_name: str = "com.example", + group_id: str = "com.example", + description: str = "" + ): + super().__init__(model, output_dir) + + self.package_name = package_name + self.spring_boot_version = spring_boot_version + self.java_version = java_version + self.app_name = app_name + self.group_id = group_id + self.description = description + + def generate(self): + self._generate_pom_file() + self._generate_mvn_files() + self._generate_main_and_test_files() + self._generate_properties_file() + self._generate_entities() + + def _generate_pom_file(self): + file_path = self.build_generation_path(file_name="pom.xml") + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path)) + pom_template = env.get_template("pom.xml.j2") + + context = { + "name": self.app_name, + "spring_boot_version": self.spring_boot_version, + "group_id": self.group_id, + "description": self.description, + "java_version": self.java_version + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = pom_template.render(**context) + f.write(generated_code) + + def _generate_mvn_files(self): + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path)) + + file_path = self.build_generation_path(file_name=self.output_dir / ".mvn" / "wrapper" / "maven-wrapper.properties") + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + maven_wrapper_template = env.get_template("maven-wrapper.properties.j2") + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = maven_wrapper_template.render() + f.write(generated_code) + + file_path = self.build_generation_path(file_name=self.output_dir / ".mvn" / "mvnw") + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + mvnw_template = env.get_template("mvnw.j2") + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = mvnw_template.render() + f.write(generated_code) + + file_path = self.build_generation_path(file_name=self.output_dir / ".mvn" / "mvnw.cmd") + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + mvnw_cmd_template = env.get_template("mvnw.cmd.j2") + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = mvnw_cmd_template.render() + f.write(generated_code) + + def _generate_main_and_test_files(self): + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path)) + + file_path = self.build_generation_path(file_name=Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / f"{self.app_name.capitalize()}.java") + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + main_template = env.get_template("main.java.j2") + + context = { + "name": self.app_name, + "package": self.package_name + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = main_template.render(**context) + f.write(generated_code) + + file_path = self.build_generation_path(file_name=Path("src", "test", "java") / Path().joinpath(*self.package_name.split(".")) / f"{self.app_name.capitalize()}Tests.java") + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + test_template = env.get_template("test.java.j2") + + context = { + "name": self.app_name, + "package": self.package_name + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = test_template.render(**context) + f.write(generated_code) + + def _generate_properties_file(self): + file_path = self.build_generation_path(file_name=Path("src", "main", "resources", "application.properties")) + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path)) + app_properties_template = env.get_template("application.properties.j2") + + context = { + "name": self.app_name, + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = app_properties_template.render(**context) + f.write(generated_code) + + def _generate_entities(self): + generator = SpringEntityGenerator( + self.model, + output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "entities", + package_name=f"{self.package_name}.entities" + ) + + generator.generate() \ No newline at end of file diff --git a/besser/generators/spring/spring_entity_generator.py b/besser/generators/spring/spring_entity_generator.py index 09a537d58..7a710fd71 100644 --- a/besser/generators/spring/spring_entity_generator.py +++ b/besser/generators/spring/spring_entity_generator.py @@ -50,7 +50,9 @@ def _generate_class_file(self, cls: Class, assocs_for_class: List[BinaryAssociat env = Environment(loader=FileSystemLoader(templates_path)) entity_template = env.get_template("entity.java.j2") - imports: set[str] = self._get_default_imports().union(self._get_specific_imports_for_class(cls)) + relations: List[object] = self._prepare_relations(cls, assocs_for_class) + + imports: set[str] = self._get_default_imports().union(self._get_specific_imports_for_class(cls, relations)) context = { "cls": cls, @@ -63,7 +65,7 @@ def _generate_class_file(self, cls: Class, assocs_for_class: List[BinaryAssociat self._prepare_attributes(cls), key=lambda a: (not a["is_id"], not a["is_enum"], not a["is_list"], a["name"]) ), - "relations": self._prepare_relations(cls, assocs_for_class), + "relations": relations, } with open(file_path, mode="w", encoding="utf-8") as f: @@ -186,23 +188,23 @@ def _generate_enum_file(self, enum: Enumeration): def _get_default_imports(self) -> set[str]: return set([ - "javax.persistence.Entity", - "javax.persistence.Table", - "javax.persistence.Id", - "javax.persistence.GeneratedValue", - "javax.persistence.GenerationType", - "javax.persistence.Column", + "jakarta.persistence.Entity", + "jakarta.persistence.Table", + "jakarta.persistence.Id", + "jakarta.persistence.GeneratedValue", + "jakarta.persistence.GenerationType", + "jakarta.persistence.Column", ]) - def _get_specific_imports_for_class(self, cls: Class) -> set[str]: + def _get_specific_imports_for_class(self, cls: Class, relations: List[object]) -> set[str]: imports: set[str] = set() if cls.is_abstract: - imports.add("javax.persistence.MappedSuperclass") + imports.add("jakarta.persistence.MappedSuperclass") if any(any(attr.type.name == enum.name for enum in self.enumerations) for attr in cls.attributes): - imports.add("javax.persistence.Enumerated") - imports.add("javax.persistence.EnumType") + imports.add("jakarta.persistence.Enumerated") + imports.add("jakarta.persistence.EnumType") for attr in cls.attributes: if attr.type.name == DateType.name: @@ -215,7 +217,28 @@ def _get_specific_imports_for_class(self, cls: Class) -> set[str]: if attr.multiplicity.max != 1: imports.add("java.util.List") imports.add("java.util.ArrayList") + + for relation in relations: + if relation["is_list"]: + imports.add("java.util.List") + imports.add("java.util.ArrayList") imports.add("java.util.Arrays") + if relation["relation"] == "OneToOne": + imports.add("jakarta.persistence.OneToOne") + if relation["owning"]: + imports.add("jakarta.persistence.JoinColumn") + if relation["relation"] == "OneToMany": + imports.add("jakarta.persistence.OneToMany") + if relation["owning"]: + imports.add("jakarta.persistence.JoinColumn") + if relation["relation"] == "ManyToOne": + imports.add("jakarta.persistence.ManyToOne") + imports.add("jakarta.persistence.JoinColumn") + if relation["relation"] == "ManyToMany": + imports.add("jakarta.persistence.ManyToMany") + if relation["owning"]: + imports.add("jakarta.persistence.JoinTable") + imports.add("jakarta.persistence.JoinColumn") return imports diff --git a/besser/generators/spring/templates/application.properties.j2 b/besser/generators/spring/templates/application.properties.j2 new file mode 100644 index 000000000..4fad7097c --- /dev/null +++ b/besser/generators/spring/templates/application.properties.j2 @@ -0,0 +1,11 @@ +spring.application.name={{ name }} + +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update + +spring.h2.console.enabled=true \ No newline at end of file diff --git a/besser/generators/spring/templates/main.java.j2 b/besser/generators/spring/templates/main.java.j2 new file mode 100644 index 000000000..72b93be73 --- /dev/null +++ b/besser/generators/spring/templates/main.java.j2 @@ -0,0 +1,13 @@ +package {{ package }}; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class {{ name | capitalize }} { + + public static void main(String[] args) { + SpringApplication.run({{ name | capitalize }}.class, args); + } + +} \ No newline at end of file diff --git a/besser/generators/spring/templates/maven-wrapper.properties.j2 b/besser/generators/spring/templates/maven-wrapper.properties.j2 new file mode 100644 index 000000000..da4c7fc43 --- /dev/null +++ b/besser/generators/spring/templates/maven-wrapper.properties.j2 @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip \ No newline at end of file diff --git a/besser/generators/spring/templates/mvnw.cmd.j2 b/besser/generators/spring/templates/mvnw.cmd.j2 new file mode 100644 index 000000000..1d8ab018e --- /dev/null +++ b/besser/generators/spring/templates/mvnw.cmd.j2 @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/besser/generators/spring/templates/mvnw.j2 b/besser/generators/spring/templates/mvnw.j2 new file mode 100644 index 000000000..8a8fb2282 --- /dev/null +++ b/besser/generators/spring/templates/mvnw.j2 @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/besser/generators/spring/templates/pom.xml.j2 b/besser/generators/spring/templates/pom.xml.j2 new file mode 100644 index 000000000..36aac81c1 --- /dev/null +++ b/besser/generators/spring/templates/pom.xml.j2 @@ -0,0 +1,54 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + {{ spring_boot_version }} + + + {{ group_id }} + {{ name }} + 0.0.1-SNAPSHOT + {{ name }} + {{ description }} + + + {{ java_version }} + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/besser/generators/spring/templates/test.java.j2 b/besser/generators/spring/templates/test.java.j2 new file mode 100644 index 000000000..66512b022 --- /dev/null +++ b/besser/generators/spring/templates/test.java.j2 @@ -0,0 +1,13 @@ +package {{ package }}; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class {{ name | capitalize }}Tests { + + @Test + void contextLoads() { + } + +} \ No newline at end of file From 217b0837691f3fb2b18a9d231727741912959fd8 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Thu, 12 Mar 2026 18:00:08 +0100 Subject: [PATCH 07/16] repository --- .../spring/backend/.idea/.gitignore | 10 -- .../{entities => entity}/Computer.java | 2 +- .../{entities => entity}/Hand.java | 2 +- .../{entities => entity}/Role.java | 2 +- .../{entities => entity}/Thing.java | 2 +- .../{entities => entity}/User.java | 54 ++++++--- .../repository/IComputerRepository.java | 11 ++ .../repository/IUserRepository.java | 19 ++++ .../com/transformers/entities/Computer.class | Bin 1626 -> 0 bytes .../com/transformers/entities/User.class | Bin 2535 -> 0 bytes .../com/transformers/entity/Computer.class | Bin 0 -> 1618 bytes .../{entities => entity}/Hand.class | Bin 1120 -> 1108 bytes .../{entities => entity}/Role.class | Bin 1120 -> 1108 bytes .../{entities => entity}/Thing.class | Bin 373 -> 369 bytes .../com/transformers/entity/User.class | Bin 0 -> 3279 bytes .../repository/IComputerRepository.class | Bin 0 -> 554 bytes .../repository/IUserRepository.class | Bin 0 -> 1535 bytes besser/generators/spring/proba.py | 13 ++- .../spring/spring_backend_generator.py | 16 ++- .../spring/spring_entity_generator.py | 12 +- .../spring/spring_repository_generator.py | 103 ++++++++++++++++++ .../spring/templates/irepository.java.j2 | 13 +++ 22 files changed, 220 insertions(+), 39 deletions(-) delete mode 100644 besser/generators/spring/backend/.idea/.gitignore rename besser/generators/spring/backend/src/main/java/com/transformers/{entities => entity}/Computer.java (97%) rename besser/generators/spring/backend/src/main/java/com/transformers/{entities => entity}/Hand.java (51%) rename besser/generators/spring/backend/src/main/java/com/transformers/{entities => entity}/Role.java (51%) rename besser/generators/spring/backend/src/main/java/com/transformers/{entities => entity}/Thing.java (90%) rename besser/generators/spring/backend/src/main/java/com/transformers/{entities => entity}/User.java (60%) create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entities/Computer.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entities/User.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entity/Computer.class rename besser/generators/spring/backend/target/classes/com/transformers/{entities => entity}/Hand.class (54%) rename besser/generators/spring/backend/target/classes/com/transformers/{entities => entity}/Role.class (54%) rename besser/generators/spring/backend/target/classes/com/transformers/{entities => entity}/Thing.class (53%) create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entity/User.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/repository/IComputerRepository.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/repository/IUserRepository.class create mode 100644 besser/generators/spring/spring_repository_generator.py create mode 100644 besser/generators/spring/templates/irepository.java.j2 diff --git a/besser/generators/spring/backend/.idea/.gitignore b/besser/generators/spring/backend/.idea/.gitignore deleted file mode 100644 index ab1f4164e..000000000 --- a/besser/generators/spring/backend/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Computer.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java similarity index 97% rename from besser/generators/spring/backend/src/main/java/com/transformers/entities/Computer.java rename to besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java index 28c83aaf6..6c31e4356 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Computer.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java @@ -1,4 +1,4 @@ -package com.transformers.entities; +package com.transformers.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Hand.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java similarity index 51% rename from besser/generators/spring/backend/src/main/java/com/transformers/entities/Hand.java rename to besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java index bbf863dd6..613a6cb53 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Hand.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java @@ -1,4 +1,4 @@ -package com.transformers.entities; +package com.transformers.entity; public enum Hand { RIGHT, LEFT diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Role.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Role.java similarity index 51% rename from besser/generators/spring/backend/src/main/java/com/transformers/entities/Role.java rename to besser/generators/spring/backend/src/main/java/com/transformers/entity/Role.java index 33f893173..0811ecb7b 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Role.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Role.java @@ -1,4 +1,4 @@ -package com.transformers.entities; +package com.transformers.entity; public enum Role { ADMIN, USER diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Thing.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Thing.java similarity index 90% rename from besser/generators/spring/backend/src/main/java/com/transformers/entities/Thing.java rename to besser/generators/spring/backend/src/main/java/com/transformers/entity/Thing.java index 52d41f587..fddf92713 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entities/Thing.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Thing.java @@ -1,4 +1,4 @@ -package com.transformers.entities; +package com.transformers.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entities/User.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java similarity index 60% rename from besser/generators/spring/backend/src/main/java/com/transformers/entities/User.java rename to besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java index c3a23d202..03a2b6558 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entities/User.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java @@ -1,4 +1,4 @@ -package com.transformers.entities; +package com.transformers.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -11,6 +11,8 @@ import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -27,12 +29,18 @@ public class User { @Enumerated(EnumType.STRING) private Role role; + @Column(name = "age", nullable = false) + public Integer age = 2; + + @Column(name = "birthday", nullable = false) + protected Duration birthday; + + @Column(name = "birthday2", nullable = false) + protected LocalDateTime birthday2; + @Column(name = "email") public String email; - @Column(name = "first_name") - public String firstName = "John"; - @ManyToMany @JoinTable(name = "many_to_many", joinColumns = @JoinColumn(name = "user_id"), @@ -41,11 +49,13 @@ public class User { public User() { } - public User(Integer id, Role role, String email, String firstName) { + public User(Integer id, Role role, Integer age, Duration birthday, LocalDateTime birthday2, String email) { this.id = id; this.role = role; + this.age = age; + this.birthday = birthday; + this.birthday2 = birthday2; this.email = email; - this.firstName = firstName; } public Integer getId() { @@ -64,20 +74,36 @@ public void setRole(Role role) { this.role = role; } - public String getEmail() { - return this.email; + public Integer getAge() { + return this.age; } - public void setEmail(String email) { - this.email = email; + public void setAge(Integer age) { + this.age = age; + } + + public Duration getBirthday() { + return this.birthday; + } + + public void setBirthday(Duration birthday) { + this.birthday = birthday; + } + + public LocalDateTime getBirthday2() { + return this.birthday2; + } + + public void setBirthday2(LocalDateTime birthday2) { + this.birthday2 = birthday2; } - public String getFirstname() { - return this.firstName; + public String getEmail() { + return this.email; } - public void setFirstname(String firstName) { - this.firstName = firstName; + public void setEmail(String email) { + this.email = email; } } \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java new file mode 100644 index 000000000..efe73885a --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java @@ -0,0 +1,11 @@ +package com.transformers.repository; + +import com.transformers.entity.Computer; +import java.util.ArrayList; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IComputerRepository extends JpaRepository { + + ArrayList findAllByModel(String model); + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java new file mode 100644 index 000000000..1beac9f0d --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java @@ -0,0 +1,19 @@ +package com.transformers.repository; + +import com.transformers.entity.Role; +import com.transformers.entity.User; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IUserRepository extends JpaRepository { + + ArrayList findAllByAge(Integer age); + ArrayList findAllByBirthday(Duration birthday); + ArrayList findAllByBirthday2(LocalDateTime birthday2); + ArrayList findAllByBirthday2Between(LocalDateTime start, LocalDateTime end); + ArrayList findAllByEmail(String email); + ArrayList findAllByRole(Role role); + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entities/Computer.class b/besser/generators/spring/backend/target/classes/com/transformers/entities/Computer.class deleted file mode 100644 index d2a1b3241aa9b6e17ce147c8f9e611018f2090c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1626 zcmah}ZEw?77(I60ni6P%Hc(#H!V07TYAAaF$|^y}rbsQCXov*gF3nx)VP8k~HBNxSVvA*?n;&pd?GjLFS;v3(tIViQ3dNoWqIRA%wue{Xyf%i0y)$WF$*h$n- zs^b(IHJonX45l1hKbdR|3=%EUt5^ArDVpr0BOS!MmmQ2HVR-l^Cs zZ%8ZD)@ECKoj$`B$6o!a=~-Fb@2McwgiR!@Asy}hPXzV4yTdG^^Yil;-A?zlgDYb- z)}tT|4Y|ukg_;s34FW~$B{mWc>RbN6D4WK-TgfxdS|YW3vT<_hUnMNt8hIDS%5EyN z+l^$Ev_dr;>i#pPRVQQCGS;iS&O$qw+VG8jk%n(|+!bR|`Dj}OJr(MpOgCI22zMX{dNw!2__+cle$f5`4tdNCk6G iEA;f>1W);>6rQ9`E`G??AG67jNqNZgBf8{--TVSAAyjAp diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entities/User.class b/besser/generators/spring/backend/target/classes/com/transformers/entities/User.class deleted file mode 100644 index 3f6242f2b9e404b71750950536a5848c90b71532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2535 zcmai#>rxa~5Xb+s>=nkKC`8ZzB8jkoqlqyZ2tv5IWI==tRhkdf07n?IJ44NdDjp?o zkyM4UDyhl?(%a&+6xWjXB^iFXAN|Wjc*rGG>~2O4&4$u(N%zH;u=0PFu3P6tkAdJ;GO3; zlpk2i4V}o(ClSPYOXq(Lva5ieBGsF^zvJl=O((F7*HRokqruNdgm7rzsG zENwXlt{p}`O+2Lu7Td+;&3xnz7Lq%ko+U#%=9=xE);t+0GL(w9>N##r;-!SypyW>I z?Z|AP>xgG@et3K79z^{jg@uS^y(1;oYf^fOLA)J{bEq5kyu3e>MqSEHDoOKQ{W8AI=2}zX zp|`(b`z)5Y-*N{ktT5!>vGFFIbH}WXAU^L);}V+%8W0ErBQ()IJ`GB7+Ka{Sk9|s7 z%ok~XDQ?bNA0Q@yN>^P-^fp5#0%F9Zk!5+T;*vF~(X{Q{@*;mvEjtoKN$%mHlySS3 zTas5$D)J5!cJ)Cxug08IQ-FJXzA|jZ9Gj7E7{`0u4WR=gobPi#s_-Sg!UKwp1(X?o zi!#@I%M#&hoG193RG7j;F8E|6M_rsB@JGdAPJFko9#}NSXO`n5dd%RlCYa*tYwo`x z3ALZ#+h1VJ;c0B_J3MRNK4%at1>`w?Po*lA49& zG{+ufIhHv}8b%3G#tgrhu{Qs$X1No~FNh|Xmn#CN~H?kB3 zg}`I*Knj_PE8>YB?6DG-*feKbr zmZSTn7qGL>4Hs;Nxt=}b)(8QAn|5>mn)^*=j%~X2KfHbq<^TWy diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entity/Computer.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/Computer.class new file mode 100644 index 0000000000000000000000000000000000000000..d0ff2f5a57491bec258965af40a4a44b06740d5b GIT binary patch literal 1618 zcmah}ZEw?77(I60ni6P%Hlw_Z@SX-%GZ@>TtP*r=iqs-RLnQciY3@>Q_H|@mZ_2*{ zX%!Na_5=2#rk%%j+q6ow54qRZ_dMsE=f(Nw-_L&o_yJGm;i6PSxq%9*4zBD*p=V=d zl3$`Y)N$fzV|_dJdV9VZI;cMNjc+#`l-jHP8s;3F|5g32ywv)E_biUpc*{@hJZdP_ zaSDwZPB(A{3l3Vdm^Y(vlv*8=Jo>{>(!t`^5vlO43)k5O&f&ZR*B?+dM+C|Yy{@r( zsN)Q@+&~jwIH-itKnD)aP2hHH%pBGoth`Q{RH*xY;=d2{Gh-sFtRERlUtqv*Dz?fS z(F(P-+0|Zmz_6QBuYT6_tgIgNRgh}J<`UMAj>i8JLA~C1ltpxZeDSi^?Y(tyd8)={ z6r`acciE^=Q^KS{plH3sCc;5|#~&JH)0lVn^Nh2GNbR0(%_{txghksRYip|Lwld>h zB&#G9s?kUfo-?UB`Lc$wTIEfa*}=k=Z}iJFe6Qo4*pj+OyDI3b*q3whvTXN!23?=& z=wK8m*ly=}9P9AJtD>2!eNM}^wtLawm5Ny$4iY6QLv6bpljZj6M9jgU@IJ}!o6}T; zbB+Yt`3Nl?<2W3mIZ_me|#e5uIkgXGp^wr6$vMfSBZ|4UPQk`;0t z*Z4WO?0kv6=QqLi0#8?YUg3U`=Sv!waRpxzb~PcU^^QS~d>aLLk$Z95%x_)!PKm!x z3Kusr%2Pz$Pj_c8x53vY8hLj)*NrlSS! zt2{Lr%@}lWm%H%YqnPmR5iR(OC-^#d_o;DjA4^BfO~wJOg83SjXnKW~MWL0m6be#} zyh8FM>AU!fsJhG7;0rOa29K+lkX;N`I)7sE10P&SCXzE)fujs6hZ)?**F^>!+@&kk z%<_?2`HKbd73M4q%^Y>;uv$12UlnPnczS?`Tu*vhI_YU`s;6)8h#C@n%+o{#b59TG g>ER4d`KT10q)slr&DQU-$&g9;p6ADO$qBptA2WVYKmY&$ literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entities/Hand.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/Hand.class similarity index 54% rename from besser/generators/spring/backend/target/classes/com/transformers/entities/Hand.class rename to besser/generators/spring/backend/target/classes/com/transformers/entity/Hand.class index 982d36f0d148daacec2ba7d907ba778f07077059..a0bf078f69d209d836ef945ada1352fed7b7f01d 100644 GIT binary patch delta 113 zcmaFBafL(R)W2Q(7#J9A8BEz3m>3ykCJKnjRO)*q=B02lFfy?4Ft9SPF*2|QdAfUq zFfz#b0Oc7gC&ubB%SS`m%kHx(YG^`vlNU2;F&a%i$EeL%Ir$G`FQdWaE+%!x%FSDt H)-VD91~VNY delta 121 zcmcb@@qk0%)W2Q(7#J9A8BEz3m>3!4CQoD%lg&&m*7r!vOW|Z-WMJW8U}a!qWMB>Q zboU5hWKfvsD9#L2Ffme>NiiD6S#Y0Gd2$1z6i8n+a diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entities/Role.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/Role.class similarity index 54% rename from besser/generators/spring/backend/target/classes/com/transformers/entities/Role.class rename to besser/generators/spring/backend/target/classes/com/transformers/entity/Role.class index 8b5ea5f596a1b5fe4b450d97380950ae49b1bb18..e4cb7160b4163acf1dbe5d792306e35685219e85 100644 GIT binary patch delta 113 zcmaFBafL(R)W2Q(7#J9A8BEz3m>3ykCJKnjRO$!i=cIBnFfy?4Ft9SPF*2|^y7+qf zF*3;c0Oc7gC&ubB%SS`m%kHx(YG^`vlNU2;F&a%i$EeL%Ir$G`FQdWaE+%!x%FSDt H)-VD92t^$u delta 121 zcmcb@@qk0%)W2Q(7#J9A8BEz3m>3!4CQoD%lg&&m)(^_hN#$fNiiD6S#Y0Gd2$1z6i8z{*9qnndZwm}{pdws2K^QWU?~jki|5@ySG2E?`D9lc`o)&Q z=v01s<$%O&AHKkm431hD!ZC$2C9jqXeA}&Wd45g%^_+GCC)mlos*AZEyP_3_W9(!w z!VP1;5jfS{yzkpPD^5M=$1r;2loJ+CVoafrM|^Aqv`r5~OGZP44bEu`XM`o;ltu5H zg>jr$Ncvutho4*NhPcLkW8g~*7jRLbchm8M_hoxWVI({#aB4cY*zoPZ@mwQ&$--q! zk!n}=hQe5j@QPQmtBZD^3sf~4SqoQiRUxHowo_F&))I~Nz<1orEKRI67`K`(I(28W zs^?wT3(TQ(`Xm)T*nVK=J~9aOTDv8k%a^HkF?Q>Tb~TZ8xhTn`SZ|#vVutD1p&E3u zu=CMqo%%Yo9I0(O(>j?)mm~JED}MZX3Er> zWGK!*5shHd>xH%a>JtWi-Kn^C(C~TSO=_{)_e$Y@*E}yo^)oxiL*u!g+wM-mlaV6h zu{_J3;}*n0N?0{YZUo+jJQW7Exfce6>54x=)c0bRY4CPzIAs=t<9=dN_5L<34D43C z5lJq1W%_$$#c}m&qqeDigCT_^wN|u!N3f!o$>6<16Wyy`E+$?0Wh#uH=@!xTpY0k# zb5-ILOb*&_NhoICwO@u7{iyDzi@eg$b@1LRzqEbUQFzN!6&>Umoa0k*9f*Zf5BEpo zd=Q%%8W3bAgC-8W{}iURP7}p&XUC6(2l>3i-(8Iv+~b7bsc7pcpONBFsF;+-e{lE0 z?k-#X!QIQ@u9CJM?G6~rY)W;$OvrbO<1CRAxNlsz#@Pd|=jaNBzQx10P!BOrJuV+% zp>?^)D6qt(%yE=jV0e-YN1D;J zNO*$lCwyy#Ht-aAuEhQ_p~U__w4dQ~ri=aTl_pMQuQqWudreLrOExi)9ZNManH@_v zF})9PoqsKWn;ihRn5f&txr0l%OQabh-Q#T5fISTlkHCl#rCw%|S1E`YlScWr%$SrV z%}mlG4$qiWr16aNu{71CWYQwda|jNihm%N-A8KUgA5BolIvtnI6Y7 ztzn&Kh;kd#*-oZqGCk|U6yBYPN#dqZh>g==C(~*y)2sM64R$iEk!igPQ+R13Cds2h z5#PteX`+*<7|ZknUdP30qLb+jncj9`3R^|QBz2|mhQbGPH`&SbE|%#>{1nGD*~zp? zrcxKCut7ykQs)Zquo1&F-O02S%k(qsIHr~x9@h9anLc!33fp4DbcIX_Y?`rTTHz6{ M%Uo+xviEKM4=6;JlK=n! literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/repository/IComputerRepository.class b/besser/generators/spring/backend/target/classes/com/transformers/repository/IComputerRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..8d168bdb67e2e93a10fd4c06e0e6afa86aaa026b GIT binary patch literal 554 zcmbVJK~BRk5L^dBXo1orPoNik;uJtCNJvGAK=cJBae{+mN1F{OH{Qj82kpnEo%#5Ddk27Xm7IR)#s7#)rDCLXAC!Putyr{&G_NEeRLI#senRKr8WgFX6 zY6i38Xf3d$jxe0{P(w=dIihGIRa5A!#F8FudM7$X=d@Og%AdLQX{swvjsf8h#OI%qPFKvT~yjI%Xcl1Y9CtZlH7Y18y gd{;Vi-371<9*o@PGZ?#P&y5HBZu;K=OjgL@7Z|Lt2mk;8 literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/repository/IUserRepository.class b/besser/generators/spring/backend/target/classes/com/transformers/repository/IUserRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..e5091842bd093743e96b782f81c1e1fba44061f1 GIT binary patch literal 1535 zcmb_c?QYXB6ul1Xy23WLF+Rrkr?i2D5z>BjAhjAu9YIVhdjPL-Gnd4!oLi}1Myw(Na&D%IT^?v)?tysuCXCchF0mYZ$&K6jQz#~fr1Z)Vz%VL zP!t6jEccZTT2a(aTcKp|a@#M;cQndS+U8EaH$oNhmbD`FRf0`&@>$}k2@XXjh-96? z6_HhGbXBMYjx2p2RIa$^&$U8oRG9Lt3(fbWxbEDxmBrIQq}0HP0IFE>{gD-@j4t)O zbmEAgsuL?%+nBTVmcg4@3E%WYv@cM8C8wnZ8&iWsKm12}1)E#*Hro=~5D{^z4UHZwq!zKfVmTg3TW@lb;7m)4x=xT%a{0GYyz*w6Au}$oAw%m9=Q| zui$%~g%YJ4-6wi#6rYQfgmqW~4;r+p5uaYy=<#5gpcPmxpz97=BWN8q3h0J|HVL{3 swayzig~1U-aD1@zcKPiQkY^%S1bt_RQ00lRnCQvd(} literal 0 HcmV?d00001 diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index 6efee46ac..7660feab1 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -7,7 +7,7 @@ from pathlib import Path from besser.BUML.metamodel.structural.structural import ( - BinaryAssociation, DateTimeType, DomainModel, Class, FloatType, Generalization, IntegerType, Multiplicity, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral + BinaryAssociation, DateTimeType, DateType, DomainModel, Class, FloatType, Generalization, IntegerType, Multiplicity, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral ) from besser.generators.spring.spring_entity_generator import SpringEntityGenerator from besser.generators.spring.spring_backend_generator import SpringBackendGenerator @@ -19,16 +19,21 @@ def proba_jednostavna_klasa(): role = Enumeration(name="Role", literals={EnumerationLiteral(name="USER"), EnumerationLiteral(name="ADMIN")}) hand = Enumeration(name="Hand", literals={EnumerationLiteral(name="LEFT"), EnumerationLiteral(name="RIGHT")}) + + # address = Class(name="Address") + # address.add_attribute(Property(name="id", type=IntegerType, is_id=True)) user = Class(name="User") user.add_attribute(Property(name="id", type=IntegerType, is_id=True)) user.add_attribute(Property(name="email", type=StringType, is_optional=True)) - user.add_attribute(Property(name="firstName", type=StringType, is_optional=True, default_value="John")) - # user.add_attribute(Property(name="age", type=IntegerType, default_value=2)) + # user.add_attribute(Property(name="firstName", type=StringType, is_optional=True, default_value="John")) + user.add_attribute(Property(name="age", type=IntegerType, default_value=2)) user.add_attribute(Property(name="role", type=role, visibility="private")) - # user.add_attribute(Property(name="birthday", type=TimeDeltaType, visibility="protected")) + user.add_attribute(Property(name="birthday", type=TimeDeltaType, visibility="protected")) + user.add_attribute(Property(name="birthday2", type=DateTimeType, visibility="protected")) # user.add_attribute(Property(name="hand", type=hand)) # user.add_attribute(Property(name="nicknames", type=DateTimeType, multiplicity=Multiplicity(0, "*"), default_value="1.2f, 3.2f")) + # user.add_attribute(Property(name="address", type=address, visibility="protected")) thing = Class(name="Thing", is_abstract=True) diff --git a/besser/generators/spring/spring_backend_generator.py b/besser/generators/spring/spring_backend_generator.py index 5b3a9e4f8..223c4c552 100644 --- a/besser/generators/spring/spring_backend_generator.py +++ b/besser/generators/spring/spring_backend_generator.py @@ -6,6 +6,7 @@ from besser.BUML.metamodel.structural.structural import DomainModel from besser.generators.generator_interface import GeneratorInterface from besser.generators.spring.spring_entity_generator import SpringEntityGenerator +from besser.generators.spring.spring_repository_generator import SpringRepositoryGenerator class SpringBackendGenerator(GeneratorInterface): @@ -34,6 +35,7 @@ def generate(self): self._generate_main_and_test_files() self._generate_properties_file() self._generate_entities() + self._generate_repositories() def _generate_pom_file(self): file_path = self.build_generation_path(file_name="pom.xml") @@ -129,8 +131,18 @@ def _generate_properties_file(self): def _generate_entities(self): generator = SpringEntityGenerator( self.model, - output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "entities", - package_name=f"{self.package_name}.entities" + output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "entity", + package_name=f"{self.package_name}.entity" + ) + + generator.generate() + + def _generate_repositories(self): + generator = SpringRepositoryGenerator( + self.model, + f"{self.package_name}.entity", + output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "repository", + package_name=f"{self.package_name}.repository" ) generator.generate() \ No newline at end of file diff --git a/besser/generators/spring/spring_entity_generator.py b/besser/generators/spring/spring_entity_generator.py index 7a710fd71..cc8effc28 100644 --- a/besser/generators/spring/spring_entity_generator.py +++ b/besser/generators/spring/spring_entity_generator.py @@ -21,12 +21,13 @@ class SpringEntityGenerator(GeneratorInterface): } def __init__(self, model: DomainModel, - output_dir: str = "./generated/entities", - package_name: str = "com.example.entities"): + output_dir: str = "./generated/entity", + package_name: str = "com.example.entity"): super().__init__(model, output_dir) self.package_name: str = package_name self.enumerations: set[Enumeration] = model.get_enumerations() + self.classes: List[Class] = model.classes_sorted_by_inheritance() self.relation_owners: dict[str, str] = self._get_relation_owner_map(model) def generate(self): @@ -38,10 +39,10 @@ def generate(self): assoc_map[end1.type.name].append(assoc) assoc_map[end2.type.name].append(assoc) - for enum in model.get_enumerations(): + for enum in self.enumerations: self._generate_enum_file(enum) - for cls in model.classes_sorted_by_inheritance(): + for cls in self.classes: self._generate_class_file(cls, assoc_map[cls.name]) def _generate_class_file(self, cls: Class, assocs_for_class: List[BinaryAssociation]): @@ -78,9 +79,10 @@ def _prepare_attributes(self, cls: Class) -> List[object]: for attr in cls.attributes: is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) is_list: bool = attr.multiplicity.max != 1 + is_class: bool = any(attr.type.name == c.name for c in self.classes) attr_type = None - if is_enum: + if is_enum or is_class: attr_type = attr.type.name else: attr_type = self.JAVA_TYPES[attr.type.name] diff --git a/besser/generators/spring/spring_repository_generator.py b/besser/generators/spring/spring_repository_generator.py new file mode 100644 index 000000000..fabf51b0f --- /dev/null +++ b/besser/generators/spring/spring_repository_generator.py @@ -0,0 +1,103 @@ +import os +from pathlib import Path +from typing import List + +from jinja2 import Environment, FileSystemLoader + +from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, StringType, TimeDeltaType, TimeType +from besser.generators.generator_interface import GeneratorInterface + +class SpringRepositoryGenerator(GeneratorInterface): + + JAVA_TYPES = { + StringType.name: "String", + BooleanType.name: "Boolean", + IntegerType.name: "Integer", + FloatType.name: "Float", + DateType.name: "LocalDate", + DateTimeType.name: "LocalDateTime", + TimeType.name: "LocalDateTime", + TimeDeltaType.name: "Duration" + } + + def __init__(self, model: DomainModel, + entity_package_name: str, + output_dir: str = "./generated/repository", + package_name: str = "com.example.repository"): + super().__init__(model, output_dir) + + self.package_name: str = package_name + self.entity_package_name: str = entity_package_name + self.enumerations: set[Enumeration] = model.get_enumerations() + self.classes: set[Class] = model.classes_sorted_by_inheritance() + + def generate(self): + model: DomainModel = self.model + + for cls in self.classes: + if not cls.is_abstract: + self._generate_repository_file(cls) + + def _generate_repository_file(self, cls: Class): + file_path = self.build_generation_path(file_name=f"I{cls.name.capitalize()}Repository.java") + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) + repository_template = env.get_template("irepository.java.j2") + + imports: set[str] = set() + imports.add("org.springframework.data.jpa.repository.JpaRepository") + imports.add(f"{self.entity_package_name}.{cls.name}") + + if cls.attributes: + imports.add("java.util.ArrayList") + + methods: List[object] = [] + + for attr in cls.attributes: + is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) + is_list: bool = attr.multiplicity.max != 1 + is_class: bool = any(attr.type.name == c.name for c in self.classes) + + if is_list or attr.is_id: + continue + + method: object = {} + + method["return_value"] = f"ArrayList<{cls.name}>" + method["name"] = f"findAllBy{attr.name.capitalize()}" + + if is_enum or is_class: + parameter_type = attr.type.name + imports.add(f"{self.entity_package_name}.{attr.type.name}") + else: + parameter_type = self.JAVA_TYPES[attr.type.name] + + method["parameter"] = f"{parameter_type} {attr.name}" + + if attr.type.name == DateType.name: + imports.add("java.time.LocalDate") + elif attr.type.name in [DateTimeType.name, TimeType.name]: + imports.add("java.time.LocalDateTime") + elif attr.type.name == TimeDeltaType.name: + imports.add("java.time.Duration") + + if attr.type.name in [DateType.name, DateTimeType.name, TimeType.name]: + methods.append({ + "return_value": f"ArrayList<{cls.name}>", + "name": f"findAllBy{attr.name.capitalize()}Between", + "parameter": f"{parameter_type} start, {parameter_type} end" + }) + + methods.append(method) + + context = { + "package": f"{self.package_name}", + "imports": sorted(imports), + "cls": cls.name, + "methods": sorted(methods, key=lambda m: m["name"]) + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = repository_template.render(**context) + f.write(generated_code) diff --git a/besser/generators/spring/templates/irepository.java.j2 b/besser/generators/spring/templates/irepository.java.j2 new file mode 100644 index 000000000..0817a7676 --- /dev/null +++ b/besser/generators/spring/templates/irepository.java.j2 @@ -0,0 +1,13 @@ +package {{ package }}; + +{% for import in imports %} +import {{ import }}; +{% endfor %} + +public interface I{{ cls }}Repository extends JpaRepository<{{ cls }}, Integer> { + + {% for method in methods %} + {{ method.return_value }} {{ method.name }}({{ method.parameter }}); + {% endfor %} + +} \ No newline at end of file From 0b39d78ad2b72c0f942c3d7f7ab48f8b953a2418 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Fri, 13 Mar 2026 13:30:51 +0100 Subject: [PATCH 08/16] service --- .../java/com/transformers/Transformers.java | 26 +++- .../repository/IComputerRepository.java | 2 + .../repository/IUserRepository.java | 2 + .../service/impl/ComputerService.java | 38 +++++ .../service/impl/UserService.java | 66 ++++++++ .../service/interfaces/IComputerService.java | 14 ++ .../service/interfaces/IUserService.java | 22 +++ .../com/transformers/Transformers.class | Bin 724 -> 2596 bytes .../repository/IComputerRepository.class | Bin 554 -> 640 bytes .../repository/IUserRepository.class | Bin 1535 -> 1621 bytes .../service/impl/ComputerService.class | Bin 0 -> 1790 bytes .../service/impl/UserService.class | Bin 0 -> 3217 bytes .../service/interfaces/IComputerService.class | Bin 0 -> 701 bytes .../service/interfaces/IUserService.class | Bin 0 -> 1666 bytes .../com/transformers/TransformersTests.class | Bin 0 -> 522 bytes .../spring/spring_backend_generator.py | 13 ++ .../spring/spring_repository_generator.py | 3 +- .../spring/spring_service_generator.py | 147 ++++++++++++++++++ .../spring/templates/irepository.java.j2 | 1 + .../spring/templates/iservice.java.j2 | 13 ++ .../spring/templates/service.java.j2 | 21 +++ 21 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/service/impl/ComputerService.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/service/impl/UserService.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IComputerService.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IUserService.class create mode 100644 besser/generators/spring/backend/target/test-classes/com/transformers/TransformersTests.class create mode 100644 besser/generators/spring/spring_service_generator.py create mode 100644 besser/generators/spring/templates/iservice.java.j2 create mode 100644 besser/generators/spring/templates/service.java.j2 diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java b/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java index 4895c0f5f..9aff1e3cf 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java @@ -1,13 +1,37 @@ package com.transformers; +import com.transformers.entity.Computer; +import com.transformers.service.interfaces.IComputerService; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; + +import java.util.ArrayList; @SpringBootApplication public class Transformers { public static void main(String[] args) { - SpringApplication.run(Transformers.class, args); + ApplicationContext context = SpringApplication.run(Transformers.class, args); + + IComputerService computerService = context.getBean(IComputerService.class); + + // kreiranje racunara + Computer c = new Computer(); + c.setModel("ThinkPad X1"); + + // cuvanje + computerService.save(c); + + System.out.println("Saved computer id: " + c.getId()); + + // pretraga + ArrayList found = computerService.findAllByModel("ThinkPad X1"); + + for (Computer computer : found) { + System.out.println(computer.getId()); + System.out.println(computer.getModel()); + } } } \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java index efe73885a..c876adc49 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java @@ -3,7 +3,9 @@ import com.transformers.entity.Computer; import java.util.ArrayList; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface IComputerRepository extends JpaRepository { ArrayList findAllByModel(String model); diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java index 1beac9f0d..9cf960796 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java @@ -6,7 +6,9 @@ import java.time.LocalDateTime; import java.util.ArrayList; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface IUserRepository extends JpaRepository { ArrayList findAllByAge(Integer age); diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java new file mode 100644 index 000000000..8352a4f73 --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java @@ -0,0 +1,38 @@ +package com.transformers.service.impl; + +import com.transformers.entity.Computer; +import com.transformers.repository.IComputerRepository; +import com.transformers.service.interfaces.IComputerService; +import java.util.ArrayList; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class ComputerService implements IComputerService { + + @Autowired + private IComputerRepository _computerRepository; + + @Override + public void delete(Computer computer) { + _computerRepository.delete(computer); + } + + @Override + public ArrayList findAllByModel(String model) { + return _computerRepository.findAllByModel(model); + } + + @Override + public Optional findById(Integer id) { + return _computerRepository.findById(id); + } + + @Override + public Computer save(Computer computer) { + return _computerRepository.save(computer); + } + + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java new file mode 100644 index 000000000..bde412bcc --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java @@ -0,0 +1,66 @@ +package com.transformers.service.impl; + +import com.transformers.entity.Role; +import com.transformers.entity.User; +import com.transformers.repository.IUserRepository; +import com.transformers.service.interfaces.IUserService; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UserService implements IUserService { + + @Autowired + private IUserRepository _userRepository; + + @Override + public void delete(User user) { + _userRepository.delete(user); + } + + @Override + public ArrayList findAllByAge(Integer age) { + return _userRepository.findAllByAge(age); + } + + @Override + public ArrayList findAllByBirthday(Duration birthday) { + return _userRepository.findAllByBirthday(birthday); + } + + @Override + public ArrayList findAllByBirthday2(LocalDateTime birthday2) { + return _userRepository.findAllByBirthday2(birthday2); + } + + @Override + public ArrayList findAllByBirthday2Between(LocalDateTime start, LocalDateTime end) { + return _userRepository.findAllByBirthday2Between(start, end); + } + + @Override + public ArrayList findAllByEmail(String email) { + return _userRepository.findAllByEmail(email); + } + + @Override + public ArrayList findAllByRole(Role role) { + return _userRepository.findAllByRole(role); + } + + @Override + public Optional findById(Integer id) { + return _userRepository.findById(id); + } + + @Override + public User save(User user) { + return _userRepository.save(user); + } + + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java new file mode 100644 index 000000000..808f2c8ef --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java @@ -0,0 +1,14 @@ +package com.transformers.service.interfaces; + +import com.transformers.entity.Computer; +import java.util.ArrayList; +import java.util.Optional; + +public interface IComputerService { + + void delete(Computer computer); + ArrayList findAllByModel(String model); + Optional findById(Integer id); + Computer save(Computer computer); + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java new file mode 100644 index 000000000..02f0b7918 --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java @@ -0,0 +1,22 @@ +package com.transformers.service.interfaces; + +import com.transformers.entity.Role; +import com.transformers.entity.User; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Optional; + +public interface IUserService { + + void delete(User user); + ArrayList findAllByAge(Integer age); + ArrayList findAllByBirthday(Duration birthday); + ArrayList findAllByBirthday2(LocalDateTime birthday2); + ArrayList findAllByBirthday2Between(LocalDateTime start, LocalDateTime end); + ArrayList findAllByEmail(String email); + ArrayList findAllByRole(Role role); + Optional findById(Integer id); + User save(User user); + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/target/classes/com/transformers/Transformers.class b/besser/generators/spring/backend/target/classes/com/transformers/Transformers.class index ec08e47ebf1eb5cbce3ae25c283b83baad193510..3ba599c62cc32e7f3eaaa8bafa7bb2c3bfc86c9d 100644 GIT binary patch literal 2596 zcma)8TXz#x6#h=yW=c8`NJ~pWpxmS>lyH+vQ>tyXQcX)GHCz=>laqAlWG2o`3Q>QE z{sN!%p;%nY<+IQJ8Xv^(Op?WfWPw>Lb7uDJv-h{ZeL3^jzc2p)a0@>q5JR7VeiH+T z3+#U?w`InbP9<|^s6KKXVxPR2_!IN zz%;QJ`vj(3uafa=p5;`^o~){8uD6xhaNQu24~6r!nr#(jV7U%mcy(SkCUdW0EZWld zvyUQ@e4veG)45)ji>?!>=Rs!Cb;?$y?#T^XMO;vy{z!>@laYSHfebaKb)?{ho{v|CvYT_7<3k>=ySaD0r7C0Dn+-9>dgd|Rw zIEhmN{l45*0!x#*cR6Mn-l?(&XYjs(vnD2y?x?YR!w*!Id3Woqw1ds)EjP2q3z);C^3D&W6ZisW zqtUmwM$NStN#F{u8u-A(HO=2}SdyLCm`z>7f$eDWZkV`<8G(Ie%PGy<_Clk@cRK2^ z&37}_dSKa^dC!xLoaG0Mbr!b_%$fKQAGLjU>M)ZQt5F87CotOPA=FuJ$+JUPz@mYV zO?-kSfm8^+LxspUrN7En2>be>-f`Q+GCpJSolrc|wjG%U1b78G&B)3dWz?$HbU~-< zj)^sVL8MGqi-S&UEfVN!ktHdR%vp|Ftyedcx2`*um~(DX+6C!Z`ne_S4>m1k@OXF0 zMF&upW~qzRJ2DF@P zBWaBnfot7>{>NCNDN$sDl-;_c8>DA>k&#YX8?`pIH+yZE?S}FuH*<8nHy_C2){3l! zX)^GbJ-(uXO}Dfr^~h&glDtITt$RhaWa;!Ch$ifUu1|sSyLAq=sw!B%rKh&zxIuU> zaPwSGX2WAd)&rLr2&cVj9V!bvUm|o9<9;pjd1%#|$oxUL0wa0Oxssl)VKq!xrI`tl zjN{t`Jop9zJx%HYr(d5(mb2|{(NA~?H3!U+EV3IL0zXC@y`|W6cUw9oTluGRuDeyQ zWt(J&jn!&}h@@lVI#bj`Jz9wE*6TL0C~Z5>X~Fo-Wyeuoc$g{%_MFF0S4_R28u(7& z*xOb4jnIq>Ct&b)_*;3=hb9;MH5DhzIpp+3PV9^fJG z7z!TYF;|0S_7%P+4Z|o?;~R#0gwK+CeYAER8z|CuiMl4Wni54xnUZ6Oz2eA@8>kpa z8Auu!GcapV4gWCqP3jB?y3+71USkBd#Z~iebFK0}$om@q4z~G};N3IA`yS)}0TIvK A=l}o! delta 206 zcmZ1?a)p)a)W2Q(7#J9A8DuANeV^RUQaRaxRd#YBTYo)+A_F@E6Htx;NHQ|8GOz(j zejraDNHYRyR;}#}j2pqy0zeWZ%>dLQ2qbwR3K)bKgn>Lx1`!5Ppdld5W?;h@7&wI3 zwRSLYh9mTIfi2?#n!pV*Lmz4g2ZI=cI8dVm5VJA@IUwgqf=MX`X&?=ff$?3=9mm3_=^Zo-%qdGDrrM=9OgTriNt}XC~#OI_Bl&mn4>C=I0eN zGHCnc7p3bL7ZhdYrKc4o=BAeC7iH@gm!uY@=9g3!r0NHy7UUOamgE;zT2Fq@s3ptH azzsBff$?3=9mm3<4Xuo-$6hWYQ93V&DdfFfs5l@PTQ50P0}}umAu6 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/repository/IUserRepository.class b/besser/generators/spring/backend/target/classes/com/transformers/repository/IUserRepository.class index e5091842bd093743e96b782f81c1e1fba44061f1..115a075b869b1119fdaa918f93c825a2a4b1b182 100644 GIT binary patch delta 128 zcmey*eU*pn)W2Q(7#J9A8Pqp&>9cwC=I0eN zGHCnc7p3bL7ZhdYrKc4o=BAeC7iH@gm!uY@=9g3!r0NHy7UUOamgE;zT2HoQ-73q> azzsB9bC5X5A{t#J~*{VPa5XPzKW~01BxH;s5{u diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/impl/ComputerService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/impl/ComputerService.class new file mode 100644 index 0000000000000000000000000000000000000000..fb2d6632d0105fe75fb798314f7a334d67257529 GIT binary patch literal 1790 zcmb7ET~8B16g{(0%1158m!P7Ef-MLmf{GR~(1e7fh@qyhW@v{pxZQ1Lr;z?DO-zhF z7@z%7#=G0HEG-p$+1r`kbN1eI&)(mEe*6UR2Cw^Jp(~E=1Y+o67&;awg4@C=^Sy&( zSu_ki>&j7PgP|*xF7%-neR1?BFn|QZV$rQ|qlM!gy1F8@$33Y}R8ewOsoFg2R;o24 zb-r=QF!GTA!PUO3x}Gwwt}`s>+Guq6H{Us9IfrBhki=jdLkZl(Fa!TjJT+C4wltDq zER}0*Ig>8<{G$nsVVog(sGQQKZEw|gT|zOWqmaDO$|+~k4VIcwHs91*)N{%+L75W? zOyV{}uTQg8-zhPqq67_@FA(fijdC4f2l%@QOyM3ww52;iGFRBu?O0hMB7o^!@BgbL5uZ3%_epkBW;` z8nzjZ7_I3VhQ7QiJHpg7b+{WH?Z14resv`q)Z+!8?$9jDQeW;f21}7IMtuHYWUlY;zQbzJro~Nj8UW&NzYk2af-~po&N^*g??C=qo@Bj2>Lj91_?+ZO(#a1 zYM!!uaF(J)abW%{;tSt#!@@a*zmS0u^kX!j8E>REX$XV`JR!P8%8D^M_}sLp&=qjM zZQ&lV@R?GgH@a kTh02DO>OR2_=WqeEzCq-kqo6two7B-WzfW0ILlYSU!+ILS^xk5 literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/impl/UserService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/impl/UserService.class new file mode 100644 index 0000000000000000000000000000000000000000..9739d4b540cb8ad1a89a67e38bf3afab1f0ad924 GIT binary patch literal 3217 zcmbW3TXWk)6vzK7Cvh%nZf#RSOG$y2T$-wd0xb?HcGG}Uw+V4#t}~2cZ=5Z%lv%Bl z@wb8*W|;E8@XUu|I4jAr5=9=<2Yap6Ilptxo{RUNfB*aoz!&&w90qcE44D|l2*dO% z`^2^!+ihCg2d{WtF^sGUSEzM{+~QJg45Jv!W8B09OopYp*RqtfUH{ONEiQe_=ki3< zxg}c1jgP5+?($>L7s`_zhSf?+UB<7ivexd27bY-?sXV4l%wU#b<$vf> z5Do5d#Tl+HR?df5SgL9E6%$u6$6y``w^4MQQm5D?h2^9|*;TyBWnn3Vw3TqIqLg;0 zB77Bi_?C%zyv;BZQ%XXrqlQiKu0$v$THM-bOIr!g?ZdiZ;vL*%m`$-hVz{4#RlK_G zY}ktL5xNg?!Ne`R$1tBJmbg0Q++`?dgTtr;_XoB3fr;C=!!Q|JeAcpsL#2~E*Q%1j zZT4||(Zmv#87{?`UC*KHe6pCXA?_-nI+hmbL%eHZ755lMHL=twHy9R^E`{ZP2C#js z6J|R>qVJn{0LsIVZ=aAOcd~U8-BifqA;XPS=|(Q=iqGRyN^VMm-h_Qc6(|qwI`_i{ zsdn3Iez#3=w|GtX;=tiW*Y#9TU_Qg6iYJ?vf2d9ZM15SNw5leTYWycCu zv8}vQA$g-f;XU;l6y8)txcp_ib-?AGO?WEGpaN>P6#6;Z98yPu?5(7e68nE{E|7^^ z**Vk)LhVUz=-I(=GYw809kHfgaCPK0c5EFbofJ8fvvnI^PPBcuiy6kMqUqYIElFZ4 znZd5eg^j+}--~tfWAq^I`YVa9iJP9`d1P*YVK9Kbv93L;$oYi9vu(<-leBvF<(q~& z>?>Q+|3r?v)I%cMK&D%7Y?+6nWHyo8Xp;2CSkQg_=F0%5J0yc)lU;qmtYO&CuH@`# zG=Tjxv+|)Ant?b*K@7{;q!Th{re&|_m->r}&`bx;2j z)7ZUmI{DPS+*6%nZbjk+`tKU0>k3VM`WX5KLhEOAj~Su$5v}92(lv&@AJdaXE2DLI z`46z)=!b#N>07@Brcj`74Ilxm(Nzfadj{(yrInx3YS22d{5$e1uW`vh51}tjz#Q4T z7GUN=(Fj9Ic!DnpS0t%nMh}`zmkZI~RiArEVXZ8jCGJ+J08MF=B=pNTgGDo+P$lUpKyq^xo$_Qs0JH2qW zWBxa^N!OJO{)cJ)!wmi|sy+U0%-^HURqSW*SJM2)8T?wZoN6)uTjD>!cXWKZ6-rUL jPDZU@8TcC?pRd^Ugo(*GA(QD&Fz`J=4g3(d?vKEKrF$X` literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IComputerService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IComputerService.class new file mode 100644 index 0000000000000000000000000000000000000000..c4ec059119ab11872777a6618fe6b34267a3f910 GIT binary patch literal 701 zcma)4O-}+b5S^}ktB4=?4@}@eIH!ttao9TRge|!Q!7jDZCFnEYe%DolZO^r>_I_}UeWQ1I5k9H~|bbR=1(s@QO zTyte8FlfxgLU1K?!Y6Mtj68!pbQgZfnJxTjP<9~*LJ&~A;kO+#;`PCD&%Df3eCp0y$$N#{EZis@QY4P~N*&n(3n zAFF>n9RG=WRF(nz`7qv#Hl)=hXm_$r=3W{t6v+Zff}Je?369KCaAH9t4vyLU;prIE zLX+7D2U3x1bN>SFm;X>=z~F?A37|j+QG^oVGNB@^hH4C|P=jDoIwj_eUI6Dr)!|}G THGZi}qORb2$B4kqO4s}WE9J`n literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IUserService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IUserService.class new file mode 100644 index 0000000000000000000000000000000000000000..b1e338f7c2f112ec7fc6ed9c537eb50be472b244 GIT binary patch literal 1666 zcmb`H+iuf95Qb;noLt({lw&!SmQ&J77$I?0sFbK61%g@;+WW1&akpBp(Rv-^SL1>Q z;Gq!XI9WFe4hC^y$-AHbnSWlbuH9YLk<9GzV-R}-jtOfby|Y`6MzF`3j{Cr%n>zkQ1O zk4&G3-Z51)K#__FRAPh#R)$=99U;0&$H!p1t$v=WhmQO-LbexcE}V{1H0kq5?-N*} zsl*;|Us4?_e1BhB{(7*0Rn%C1@-OldHXEtUZqmc+nyJ|DTK?!u+GJEq7|N7oIyf#@dOnjqVP+lV#|zJvHK+{=~s4SIlR3m)cZ+n^mp YkKl2Ro*48L(KBexoCz0g{J?_UUj)SK^8f$< literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/test-classes/com/transformers/TransformersTests.class b/besser/generators/spring/backend/target/test-classes/com/transformers/TransformersTests.class new file mode 100644 index 0000000000000000000000000000000000000000..349a01bc7f3da25fff34dbcd5ae7a9e299c12c13 GIT binary patch literal 522 zcmah_O-lnY5PjL!R=3sGDuUoa#DgDTZeIKV6+A5pw)CFerZu&jlx$Z0Tb=|D{s4cJ zIEfVsJ($Cr%$s?Snfdzu_yll)%@`3XbyS;JLXDw47SDoDgdXwxbSyK+P&-#zxl4x1 z!C}9SWrn@XGzx5eIGc(%BSC<$uAl3Fmsh!GYUlochue3H!IAt`|b`oPpd|c8Bgj40D z;~`yqnCOO literal 0 HcmV?d00001 diff --git a/besser/generators/spring/spring_backend_generator.py b/besser/generators/spring/spring_backend_generator.py index 223c4c552..d670ec7ee 100644 --- a/besser/generators/spring/spring_backend_generator.py +++ b/besser/generators/spring/spring_backend_generator.py @@ -7,6 +7,7 @@ from besser.generators.generator_interface import GeneratorInterface from besser.generators.spring.spring_entity_generator import SpringEntityGenerator from besser.generators.spring.spring_repository_generator import SpringRepositoryGenerator +from besser.generators.spring.spring_service_generator import SpringServiceGenerator class SpringBackendGenerator(GeneratorInterface): @@ -36,6 +37,7 @@ def generate(self): self._generate_properties_file() self._generate_entities() self._generate_repositories() + self._generate_services() def _generate_pom_file(self): file_path = self.build_generation_path(file_name="pom.xml") @@ -145,4 +147,15 @@ def _generate_repositories(self): package_name=f"{self.package_name}.repository" ) + generator.generate() + + def _generate_services(self): + generator = SpringServiceGenerator( + self.model, + f"{self.package_name}.entity", + f"{self.package_name}.repository", + output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "service", + package_name=f"{self.package_name}.service" + ) + generator.generate() \ No newline at end of file diff --git a/besser/generators/spring/spring_repository_generator.py b/besser/generators/spring/spring_repository_generator.py index fabf51b0f..4e551335d 100644 --- a/besser/generators/spring/spring_repository_generator.py +++ b/besser/generators/spring/spring_repository_generator.py @@ -32,8 +32,6 @@ def __init__(self, model: DomainModel, self.classes: set[Class] = model.classes_sorted_by_inheritance() def generate(self): - model: DomainModel = self.model - for cls in self.classes: if not cls.is_abstract: self._generate_repository_file(cls) @@ -47,6 +45,7 @@ def _generate_repository_file(self, cls: Class): imports: set[str] = set() imports.add("org.springframework.data.jpa.repository.JpaRepository") + imports.add("org.springframework.stereotype.Repository") imports.add(f"{self.entity_package_name}.{cls.name}") if cls.attributes: diff --git a/besser/generators/spring/spring_service_generator.py b/besser/generators/spring/spring_service_generator.py new file mode 100644 index 000000000..90d8f90a7 --- /dev/null +++ b/besser/generators/spring/spring_service_generator.py @@ -0,0 +1,147 @@ +import os +from pathlib import Path +from typing import List + +from jinja2 import Environment, FileSystemLoader + +from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, StringType, TimeDeltaType, TimeType +from besser.generators.generator_interface import GeneratorInterface + +class SpringServiceGenerator(GeneratorInterface): + + JAVA_TYPES = { + StringType.name: "String", + BooleanType.name: "Boolean", + IntegerType.name: "Integer", + FloatType.name: "Float", + DateType.name: "LocalDate", + DateTimeType.name: "LocalDateTime", + TimeType.name: "LocalDateTime", + TimeDeltaType.name: "Duration" + } + + def __init__(self, model: DomainModel, + entity_package_name: str, + repository_package_name: str, + output_dir: str = "./generated/service", + package_name: str = "com.example.service"): + super().__init__(model, output_dir) + + self.package_name: str = package_name + self.entity_package_name: str = entity_package_name + self.repository_package_name: str = repository_package_name + self.enumerations: set[Enumeration] = model.get_enumerations() + self.classes: set[Class] = model.classes_sorted_by_inheritance() + + def generate(self): + for cls in self.classes: + if not cls.is_abstract: + self._generate_service_files(cls) + + def _generate_service_files(self, cls: Class): + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) + + file_path = self.build_generation_path(file_name=Path("interfaces") / f"I{cls.name.capitalize()}Service.java") + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + iservice_template = env.get_template("iservice.java.j2") + + imports: set[str] = set() + imports.add(f"{self.entity_package_name}.{cls.name}") + + if cls.attributes: + imports.add("java.util.ArrayList") + imports.add("java.util.Optional") + + methods: List[object] = [] + + for attr in cls.attributes: + is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) + is_list: bool = attr.multiplicity.max != 1 + is_class: bool = any(attr.type.name == c.name for c in self.classes) + + if is_list or attr.is_id: + continue + + method: object = {} + + method["return_value"] = f"ArrayList<{cls.name}>" + method["name"] = f"findAllBy{attr.name.capitalize()}" + + if is_enum or is_class: + parameter_type = attr.type.name + imports.add(f"{self.entity_package_name}.{attr.type.name}") + else: + parameter_type = self.JAVA_TYPES[attr.type.name] + + method["parameter"] = f"{parameter_type} {attr.name}" + + if attr.type.name == DateType.name: + imports.add("java.time.LocalDate") + elif attr.type.name in [DateTimeType.name, TimeType.name]: + imports.add("java.time.LocalDateTime") + elif attr.type.name == TimeDeltaType.name: + imports.add("java.time.Duration") + + if attr.type.name in [DateType.name, DateTimeType.name, TimeType.name]: + methods.append({ + "return_value": f"ArrayList<{cls.name}>", + "name": f"findAllBy{attr.name.capitalize()}Between", + "parameter": f"{parameter_type} start, {parameter_type} end" + }) + + methods.append(method) + + methods.extend(self._get_crud_methods(cls)) + + context = { + "package": f"{self.package_name}.interfaces", + "imports": sorted(imports), + "cls": cls.name, + "methods": sorted(methods, key=lambda m: m["name"]) + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = iservice_template.render(**context) + f.write(generated_code) + + file_path = self.build_generation_path(file_name=Path("impl") / f"{cls.name.capitalize()}Service.java") + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + service_template = env.get_template("service.java.j2") + + context["package"] = f"{self.package_name}.impl" + + imports.add("org.springframework.beans.factory.annotation.Autowired") + imports.add("org.springframework.stereotype.Service") + imports.add(f"{self.package_name}.interfaces.I{cls.name.capitalize()}Service") + imports.add(f"{self.repository_package_name}.I{cls.name.capitalize()}Repository") + + context["imports"] = sorted(imports) + + for method in context["methods"]: + parameter: str = method["parameter"] + tokens: List[str] = parameter.split(", ") + method["parameter_names"] = ", ".join(token.split(" ")[1] for token in tokens) + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = service_template.render(**context) + f.write(generated_code) + + def _get_crud_methods(self, cls: Class) -> List[object]: + return [ + { + "return_value": f"Optional<{cls.name}>", + "name": "findById", + "parameter": "Integer id" + }, + { + "return_value": f"{cls.name}", + "name": "save", + "parameter": f"{cls.name} {cls.name[0].lower() + cls.name[1:]}" + }, + { + "return_value": "void", + "name": "delete", + "parameter": f"{cls.name} {cls.name[0].lower() + cls.name[1:]}" + } + ] diff --git a/besser/generators/spring/templates/irepository.java.j2 b/besser/generators/spring/templates/irepository.java.j2 index 0817a7676..cb621436c 100644 --- a/besser/generators/spring/templates/irepository.java.j2 +++ b/besser/generators/spring/templates/irepository.java.j2 @@ -4,6 +4,7 @@ package {{ package }}; import {{ import }}; {% endfor %} +@Repository public interface I{{ cls }}Repository extends JpaRepository<{{ cls }}, Integer> { {% for method in methods %} diff --git a/besser/generators/spring/templates/iservice.java.j2 b/besser/generators/spring/templates/iservice.java.j2 new file mode 100644 index 000000000..05e2c1f0c --- /dev/null +++ b/besser/generators/spring/templates/iservice.java.j2 @@ -0,0 +1,13 @@ +package {{ package }}; + +{% for import in imports %} +import {{ import }}; +{% endfor %} + +public interface I{{ cls }}Service { + + {% for method in methods %} + {{ method.return_value }} {{ method.name }}({{ method.parameter }}); + {% endfor %} + +} \ No newline at end of file diff --git a/besser/generators/spring/templates/service.java.j2 b/besser/generators/spring/templates/service.java.j2 new file mode 100644 index 000000000..7b44a32d0 --- /dev/null +++ b/besser/generators/spring/templates/service.java.j2 @@ -0,0 +1,21 @@ +package {{ package }}; + +{% for import in imports %} +import {{ import }}; +{% endfor %} + +@Service +public class {{ cls }}Service implements I{{ cls }}Service { + + @Autowired + private I{{ cls }}Repository _{{ cls[0] | lower + cls[1:] }}Repository; + + {% for method in methods %} +@Override + public {{ method.return_value }} {{ method.name }}({{ method.parameter }}) { + {% if method.return_value != "void" %}return {% endif %}_{{ cls[0] | lower + cls[1:] }}Repository.{{ method.name }}({{ method.parameter_names }}); + } + + {% endfor %} + +} \ No newline at end of file From 4f5059c7b5ed4c1de532824d84a07be4aec1d42c Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Fri, 13 Mar 2026 15:59:51 +0100 Subject: [PATCH 09/16] controllers, http --- .../java/com/transformers/Transformers.java | 26 +------ .../controller/ComputerController.java | 70 ++++++++++++++++++ .../controller/UserController.java | 70 ++++++++++++++++++ .../com/transformers/entity/Computer.java | 8 -- .../java/com/transformers/entity/Hand.java | 2 +- .../java/com/transformers/entity/User.java | 12 --- .../com/transformers/http_test/computer.http | 27 +++++++ .../java/com/transformers/http_test/user.http | 27 +++++++ .../service/impl/ComputerService.java | 6 ++ .../service/impl/UserService.java | 6 ++ .../service/interfaces/IComputerService.java | 2 + .../service/interfaces/IUserService.java | 2 + .../com/transformers/Transformers.class | Bin 2596 -> 724 bytes .../controller/ComputerController.class | Bin 0 -> 4704 bytes .../controller/UserController.class | Bin 0 -> 4620 bytes .../com/transformers/entity/Computer.class | Bin 1618 -> 1374 bytes .../com/transformers/entity/Hand.class | Bin 1108 -> 1108 bytes .../com/transformers/entity/User.class | Bin 3279 -> 2866 bytes .../service/impl/ComputerService.class | Bin 1790 -> 1962 bytes .../service/impl/UserService.class | Bin 3217 -> 3385 bytes .../service/interfaces/IComputerService.class | Bin 701 -> 805 bytes .../service/interfaces/IUserService.class | Bin 1666 -> 1766 bytes besser/generators/spring/endpoint.http | 27 +++++++ besser/generators/spring/proba.py | 10 +-- .../spring/spring_backend_generator.py | 23 ++++++ .../spring/spring_controller_generator.py | 61 +++++++++++++++ .../spring/spring_http_generator.py | 36 +++++++++ .../spring/spring_service_generator.py | 8 +- .../spring/templates/controller.java.j2 | 59 +++++++++++++++ .../generators/spring/templates/http.http.j2 | 27 +++++++ 30 files changed, 457 insertions(+), 52 deletions(-) create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/controller/ComputerController.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/controller/UserController.java create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/http_test/computer.http create mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/http_test/user.http create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/controller/ComputerController.class create mode 100644 besser/generators/spring/backend/target/classes/com/transformers/controller/UserController.class create mode 100644 besser/generators/spring/endpoint.http create mode 100644 besser/generators/spring/spring_controller_generator.py create mode 100644 besser/generators/spring/spring_http_generator.py create mode 100644 besser/generators/spring/templates/controller.java.j2 create mode 100644 besser/generators/spring/templates/http.http.j2 diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java b/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java index 9aff1e3cf..4895c0f5f 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java @@ -1,37 +1,13 @@ package com.transformers; -import com.transformers.entity.Computer; -import com.transformers.service.interfaces.IComputerService; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.ApplicationContext; - -import java.util.ArrayList; @SpringBootApplication public class Transformers { public static void main(String[] args) { - ApplicationContext context = SpringApplication.run(Transformers.class, args); - - IComputerService computerService = context.getBean(IComputerService.class); - - // kreiranje racunara - Computer c = new Computer(); - c.setModel("ThinkPad X1"); - - // cuvanje - computerService.save(c); - - System.out.println("Saved computer id: " + c.getId()); - - // pretraga - ArrayList found = computerService.findAllByModel("ThinkPad X1"); - - for (Computer computer : found) { - System.out.println(computer.getId()); - System.out.println(computer.getModel()); - } + SpringApplication.run(Transformers.class, args); } } \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/controller/ComputerController.java b/besser/generators/spring/backend/src/main/java/com/transformers/controller/ComputerController.java new file mode 100644 index 000000000..e3f1e3886 --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/controller/ComputerController.java @@ -0,0 +1,70 @@ +package com.transformers.controller; + +import com.transformers.entity.Computer; +import com.transformers.service.interfaces.IComputerService; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/computer") +public class ComputerController { + + @Autowired + private IComputerService _computerService; + + @GetMapping + public ResponseEntity> getAll() { + return ResponseEntity.ok(_computerService.findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Integer id) { + return _computerService.findById(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity create(@RequestBody Computer entity) { + Computer saved = _computerService.save(entity); + return ResponseEntity.ok(saved); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Integer id, @RequestBody Computer entity) { + + Optional existing = _computerService.findById(id); + + if (existing.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + entity.setId(id); + Computer updated = _computerService.save(entity); + + return ResponseEntity.ok(updated); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteById(@PathVariable Integer id) { + + Optional existing = _computerService.findById(id); + + if (existing.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + _computerService.delete(existing.get()); + + return ResponseEntity.noContent().build(); + } + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/controller/UserController.java b/besser/generators/spring/backend/src/main/java/com/transformers/controller/UserController.java new file mode 100644 index 000000000..a8db943be --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/controller/UserController.java @@ -0,0 +1,70 @@ +package com.transformers.controller; + +import com.transformers.entity.User; +import com.transformers.service.interfaces.IUserService; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/user") +public class UserController { + + @Autowired + private IUserService _userService; + + @GetMapping + public ResponseEntity> getAll() { + return ResponseEntity.ok(_userService.findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Integer id) { + return _userService.findById(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity create(@RequestBody User entity) { + User saved = _userService.save(entity); + return ResponseEntity.ok(saved); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Integer id, @RequestBody User entity) { + + Optional existing = _userService.findById(id); + + if (existing.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + entity.setId(id); + User updated = _userService.save(entity); + + return ResponseEntity.ok(updated); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteById(@PathVariable Integer id) { + + Optional existing = _userService.findById(id); + + if (existing.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + _userService.delete(existing.get()); + + return ResponseEntity.noContent().build(); + } + +} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java index 6c31e4356..97ce5a003 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java @@ -5,11 +5,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; @Entity @Table(name = "computers") @@ -23,10 +19,6 @@ public class Computer extends Thing { @Column(name = "model", nullable = false) public String model; - @ManyToMany(mappedBy = "computers") - - private List owners = new ArrayList<>(); - public Computer() { } public Computer(Integer id, String model) { diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java index 613a6cb53..bc33a4f91 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java @@ -1,5 +1,5 @@ package com.transformers.entity; public enum Hand { - RIGHT, LEFT + LEFT, RIGHT } \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java index 03a2b6558..23aa4cef4 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java @@ -7,15 +7,9 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; -import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; import java.time.Duration; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; @Entity @Table(name = "users") @@ -41,12 +35,6 @@ public class User { @Column(name = "email") public String email; - @ManyToMany - @JoinTable(name = "many_to_many", - joinColumns = @JoinColumn(name = "user_id"), - inverseJoinColumns = @JoinColumn(name = "computer_id")) - private List computers = new ArrayList<>(); - public User() { } public User(Integer id, Role role, Integer age, Duration birthday, LocalDateTime birthday2, String email) { diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/http_test/computer.http b/besser/generators/spring/backend/src/main/java/com/transformers/http_test/computer.http new file mode 100644 index 000000000..14feebbec --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/http_test/computer.http @@ -0,0 +1,27 @@ +### GET ALL +GET http://localhost:8080/api/computer +Accept: application/json + +### GET BY ID +GET http://localhost:8080/api/computer/ +Accept: application/json + +### CREATE +POST http://localhost:8080/api/computer +Content-Type: application/json + +{ + +} + +### UPDATE +PUT http://localhost:8080/api/computer/ +Content-Type: application/json + +{ + +} + +### DELETE +DELETE http://localhost:8080/api/computer/ +Accept: application/json \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/http_test/user.http b/besser/generators/spring/backend/src/main/java/com/transformers/http_test/user.http new file mode 100644 index 000000000..8bd7f242f --- /dev/null +++ b/besser/generators/spring/backend/src/main/java/com/transformers/http_test/user.http @@ -0,0 +1,27 @@ +### GET ALL +GET http://localhost:8080/api/user +Accept: application/json + +### GET BY ID +GET http://localhost:8080/api/user/ +Accept: application/json + +### CREATE +POST http://localhost:8080/api/user +Content-Type: application/json + +{ + +} + +### UPDATE +PUT http://localhost:8080/api/user/ +Content-Type: application/json + +{ + +} + +### DELETE +DELETE http://localhost:8080/api/user/ +Accept: application/json \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java index 8352a4f73..1f700948d 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java @@ -4,6 +4,7 @@ import com.transformers.repository.IComputerRepository; import com.transformers.service.interfaces.IComputerService; import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -19,6 +20,11 @@ public void delete(Computer computer) { _computerRepository.delete(computer); } + @Override + public List findAll() { + return _computerRepository.findAll(); + } + @Override public ArrayList findAllByModel(String model) { return _computerRepository.findAllByModel(model); diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java index bde412bcc..3cda081d2 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java @@ -7,6 +7,7 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -22,6 +23,11 @@ public void delete(User user) { _userRepository.delete(user); } + @Override + public List findAll() { + return _userRepository.findAll(); + } + @Override public ArrayList findAllByAge(Integer age) { return _userRepository.findAllByAge(age); diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java index 808f2c8ef..65d57fa49 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java @@ -2,11 +2,13 @@ import com.transformers.entity.Computer; import java.util.ArrayList; +import java.util.List; import java.util.Optional; public interface IComputerService { void delete(Computer computer); + List findAll(); ArrayList findAllByModel(String model); Optional findById(Integer id); Computer save(Computer computer); diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java index 02f0b7918..1a2f5de89 100644 --- a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java +++ b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java @@ -5,11 +5,13 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.List; import java.util.Optional; public interface IUserService { void delete(User user); + List findAll(); ArrayList findAllByAge(Integer age); ArrayList findAllByBirthday(Duration birthday); ArrayList findAllByBirthday2(LocalDateTime birthday2); diff --git a/besser/generators/spring/backend/target/classes/com/transformers/Transformers.class b/besser/generators/spring/backend/target/classes/com/transformers/Transformers.class index 3ba599c62cc32e7f3eaaa8bafa7bb2c3bfc86c9d..ec08e47ebf1eb5cbce3ae25c283b83baad193510 100644 GIT binary patch delta 206 zcmZ1?a)p)a)W2Q(7#J9A8DuANeV^RUQaRaxRd#YBTYo)+A_F@E6Htx;NHQ|8GOz(j zejraDNHYRyR;}#}j2pqy0zeWZ%>dLQ2qbwR3K)bKgn>Lx1`!5Ppdld5W?;h@7&wI3 zwRSLYh9mTIfi2?#n!pV*Lmz4g2ZI=cI8dVm5VJA@IUwgqf=MX`X&?=lyH+vQ>tyXQcX)GHCz=>laqAlWG2o`3Q>QE z{sN!%p;%nY<+IQJ8Xv^(Op?WfWPw>Lb7uDJv-h{ZeL3^jzc2p)a0@>q5JR7VeiH+T z3+#U?w`InbP9<|^s6KKXVxPR2_!IN zz%;QJ`vj(3uafa=p5;`^o~){8uD6xhaNQu24~6r!nr#(jV7U%mcy(SkCUdW0EZWld zvyUQ@e4veG)45)ji>?!>=Rs!Cb;?$y?#T^XMO;vy{z!>@laYSHfebaKb)?{ho{v|CvYT_7<3k>=ySaD0r7C0Dn+-9>dgd|Rw zIEhmN{l45*0!x#*cR6Mn-l?(&XYjs(vnD2y?x?YR!w*!Id3Woqw1ds)EjP2q3z);C^3D&W6ZisW zqtUmwM$NStN#F{u8u-A(HO=2}SdyLCm`z>7f$eDWZkV`<8G(Ie%PGy<_Clk@cRK2^ z&37}_dSKa^dC!xLoaG0Mbr!b_%$fKQAGLjU>M)ZQt5F87CotOPA=FuJ$+JUPz@mYV zO?-kSfm8^+LxspUrN7En2>be>-f`Q+GCpJSolrc|wjG%U1b78G&B)3dWz?$HbU~-< zj)^sVL8MGqi-S&UEfVN!ktHdR%vp|Ftyedcx2`*um~(DX+6C!Z`ne_S4>m1k@OXF0 zMF&upW~qzRJ2DF@P zBWaBnfot7>{>NCNDN$sDl-;_c8>DA>k&#YX8?`pIH+yZE?S}FuH*<8nHy_C2){3l! zX)^GbJ-(uXO}Dfr^~h&glDtITt$RhaWa;!Ch$ifUu1|sSyLAq=sw!B%rKh&zxIuU> zaPwSGX2WAd)&rLr2&cVj9V!bvUm|o9<9;pjd1%#|$oxUL0wa0Oxssl)VKq!xrI`tl zjN{t`Jop9zJx%HYr(d5(mb2|{(NA~?H3!U+EV3IL0zXC@y`|W6cUw9oTluGRuDeyQ zWt(J&jn!&}h@@lVI#bj`Jz9wE*6TL0C~Z5>X~Fo-Wyeuoc$g{%_MFF0S4_R28u(7& z*xOb4jnIq>Ct&b)_*;3=hb9;MH5DhzIpp+3PV9^fJG z7z!TYF;|0S_7%P+4Z|o?;~R#0gwK+CeYAER8z|CuiMl4Wni54xnUZ6Oz2eA@8>kpa z8Auu!GcapV4gWCqP3jB?y3+71USkBd#Z~iebFK0}$om@q4z~G};N3IA`yS)}0TIvK A=l}o! diff --git a/besser/generators/spring/backend/target/classes/com/transformers/controller/ComputerController.class b/besser/generators/spring/backend/target/classes/com/transformers/controller/ComputerController.class new file mode 100644 index 0000000000000000000000000000000000000000..ad34ac7516b7c75d6b670ece715000950f34db63 GIT binary patch literal 4704 zcmcIn{Z|~-8Gh~t*Z@PaB{3SKkyuP1U?wKy!$2^A!BCfkLc~_BW_I@i6J}>N^Fc&w zwe?$DKic~BtABx><4Hq)=<(;C{z*MOeeTTc?7&WvJ*jdId*|Le@7MD_?|bk3>%X`D z3E(Wg=|T?M@@Ui0jt+ssRb$Q2EyG^cFO{y!vMLy#^}j-iaCJetYi3yroGdme@uEH5 zv?;!1l%=Q7)V0NOqutnnop}^A`~tlKBln_2AireVl?lrd=p7g=hQSBEY3W7N^GCa| z8~5e0N5lPiK;V$$F6&;+HSOgk*Qmci0OhsTYt#pR)EK8SK8;)P9`KDtV)@U#DyI;d2=o4r+ zYBh^~J+djrrNAz$m-XrRfdM~?U*^%T;V~Q}B+Y8cf~rvyI3D{Eg4L|(HXaEKXc)v{ zYFU<~)@}S*2`hGuFtv z>49eA$uNNlbBSydt+u|Rd>qkm0w>8P(>q_SDF(1sUQqNot>HpLPP9DLlg@ zw8(<~3(}~tswM-|;z&}A;cOn`8lJ*AR?fYtAs<)Z1CB}-1&OpBpvz!Nx~nU#`cbJN*_e8*kajYL472z=+d>B`C|88zip z=-bYsY0Ha2wItok2Gz5IicZcgH;nYtIh>;**5&Z zr46I?B#xyTh3ar)%`R-E_xR{I4Uc4Eb6uA75?fOm7S`u1yA69J#q#tw>RaQP9LUs~ z(NU=?XOc;~QXRG4{CK9!N`J+v%o(aF^S~gsrT@!ZOR$jNYlamt0qy$hX5|fmM>mrz z@n;juPG-iOh2)&!uhf~Na$k0(p(>!G=yZ3qTJH0-wOzi-f@ImS8K$ZvQNJNha%CTF*`i?qH2m$+jhe|@eROGOvz>Gt-{Y0P7Ij^(fVTw>rXh|# z9O}2y^ohQ+1G@@XE?|aFA9V3s;zJFu<6HsfxbWvL`~{!n@u`N-@HzWYc>N6JO?%B* zm3lG6j)sl+Ko;m`%Wus7o%SOZiu}A_*cD59{YA%F4QivQgJi*1H~RF-Cc(=ruY22! zOx{^GeN~OkP8pW9VEU4I(`Ia&-%pm|d6K8vj?LG>Jig$4s0pO3c=Py5VE>(8WxtES z!2u>F$Il0zy6Q8JHCS}{XcC3NV#m8^p_&3mQ;?xGn zz3+yHZeT*~4{w0Jo@v|L7JJ(kruac785pKFN6Px z7>DB4A=Ge&%5&Xhgl1Wa2tS)0ybW zaUse?gu$x`N4_Nn3g3|i2D8IkFbKoHfoFci=6{2M0K+zs@A9 zoQPC~_A!j#b1jEALe0H=`UB^0CfeV^A30Y=@iF206W+nQ;nRC~A0Oeb+*2}Rto6Tj NbmH&$5?^EQ{{U^(H@N@+ literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/controller/UserController.class b/besser/generators/spring/backend/target/classes/com/transformers/controller/UserController.class new file mode 100644 index 0000000000000000000000000000000000000000..d59d4d5c18872ac0b59f45a6db293d494b485107 GIT binary patch literal 4620 zcmcIn=~Em<9DTh3HVZ?tk{FHAgeWEuFcS$$G!P764s=NnBHqXBZUYl$XVy80c;EMZ z=c|8#R#`FRgXL$-KgqIuJu~diW;bNLsA7A%d-`|0-|N>szx{dT7XZg_u^lOFNTWqV zE7}CI=Z$4Uw+wqhKbt=<3%)?xh-sVtsKAEazPSuGB9lhDh7M=~`wC7;_g%yG<{h^r zT~9AKw(mNYC0+etPr4J8u7LJTz>!($E}I1@Fqo@b%xN{Jo3=0Ad7~gbeNtJ8*9>=H z6E>%j)o=s41P=UN0|M!J(=Lu#mOxi;UoK2G@J&n4nVvt~j;**cjcpok!p#DE9d|+Z z%C2cI%)3TOUU1wcebM*J`i%6-j_t`4wr~0?0x4%n;8<^DF9F_gU#>|TVQRN%*nwLG zHmV@US0;-BeMzH}gk?dxv|0`PY}q#*+pvbaSkIjrcA;CK)hL%O#4C%nW&uR|W<8n-7IG7{^2!_iH$TlLEJ{2MRrS(pNm)){v)j3bfOT z0+Rw;*T{HvogUPX!xV+4C@tx;QoFBLDcaqNvl^xq8_nblV z+%Y+)U|4g8YpQ-c*y1mm^fyq?r)!EMeYA0hgu;}WH5Y8d4_w-Rs`q0_4FB?`s;9K<<&Em@fx7QM?5=#xU4mXYBwBawVu|?Iq z;7UU=e)4*ZZ|I@HS)`0cWfs`EJ_FR|r7#kH^S|=m1i*(HX3~gp(~g&*T=VN@xG|jyG z!*i%AcUj9Q<%>p7ta?2M1lnhvz%9s=raA?-)ZTRWD@DB4dDDpLyiWSni!#a7_^4{U zG$ea9){JhHOTu+3CpTtviqoSIj1(m^0Ww?J8+f%rL+ki{heFb|} z+y5)JUhSzn^t3Hn8GE7)JdI}<=Cicd%IgA~$h{G|-^8gW_@C#GhXRV6Pph;f9|Y#H zkcepm$FgxudJ0nmHTkOVX7IDA1AD?qx(O5CuSAl?B23!spvCiiO0C7T$k(EnmMV}~ z9IM1qVn@J+6UTCyYg;)sSjFeeU6*k1BDBjmvI=tVhk^b}7*q2Dt6;20Tei2v(YAyo zzJ)~w1{lqLbdd);Fo`3$G|15i%R9J;-*2|SjT;S4VB|1|ah$<0b@O*_M-xC9c?@$$kse!97 z2-ClWQ@>(-6%V9H&odD?To<9REzEM5@g5@zqeNj0{g_D9Z>WO6P>etaKVDScsYJGF z#OFwTwl$nvsbu=XRhh1FF59jYm6K{8>}IR}GL@>bBN98bn`yknu@qhoEqC$h74~1P rSbq(#v#&PAhlJ}5yoI;Jr+4r!KEM~8Q)@=J*S~Db;46HC@38$Z@<9{z literal 0 HcmV?d00001 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entity/Computer.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/Computer.class index d0ff2f5a57491bec258965af40a4a44b06740d5b..b0a168834d9f6a50271f352a3afc4773d820e389 100644 GIT binary patch delta 665 zcmZ{iTQUPt5QhIbF;_Dgf=GmjOM;n@4dB@pcvC8m$|d17YnYu#ky53U1$eXowqOOy z-!liH#!L6SFPF zH_Ub|W=x2!QKUm}(3{acj3EXxY>~0-gUU;^GME*pb2PX%(O{KyAJP}-L+Os<5$vcv zLNH`1lPO_>>k4fZF42l?6Uj!_rX?w$&&T;;O0Rf$z*WpkO2)2YjrKS69qb!!jx*PD znb*qkx(6AOh9GMh;Z)7pj#DwTUgxkQ04D n9j8=d7no8ZB2@ejRQA>yN^VNATx^{FmXND9<85N{i|l*?Qo1j0 literal 1618 zcmah}ZEw?77(I60ni6P%Hlw_Z@SX-%GZ@>TtP*r=iqs-RLnQciY3@>Q_H|@mZ_2*{ zX%!Na_5=2#rk%%j+q6ow54qRZ_dMsE=f(Nw-_L&o_yJGm;i6PSxq%9*4zBD*p=V=d zl3$`Y)N$fzV|_dJdV9VZI;cMNjc+#`l-jHP8s;3F|5g32ywv)E_biUpc*{@hJZdP_ zaSDwZPB(A{3l3Vdm^Y(vlv*8=Jo>{>(!t`^5vlO43)k5O&f&ZR*B?+dM+C|Yy{@r( zsN)Q@+&~jwIH-itKnD)aP2hHH%pBGoth`Q{RH*xY;=d2{Gh-sFtRERlUtqv*Dz?fS z(F(P-+0|Zmz_6QBuYT6_tgIgNRgh}J<`UMAj>i8JLA~C1ltpxZeDSi^?Y(tyd8)={ z6r`acciE^=Q^KS{plH3sCc;5|#~&JH)0lVn^Nh2GNbR0(%_{txghksRYip|Lwld>h zB&#G9s?kUfo-?UB`Lc$wTIEfa*}=k=Z}iJFe6Qo4*pj+OyDI3b*q3whvTXN!23?=& z=wK8m*ly=}9P9AJtD>2!eNM}^wtLawm5Ny$4iY6QLv6bpljZj6M9jgU@IJ}!o6}T; zbB+Yt`3Nl?<2W3mIZ_me|#e5uIkgXGp^wr6$vMfSBZ|4UPQk`;0t z*Z4WO?0kv6=QqLi0#8?YUg3U`=Sv!waRpxzb~PcU^^QS~d>aLLk$Z95%x_)!PKm!x z3Kusr%2Pz$Pj_c8x53vY8hLj)*NrlSS! zt2{Lr%@}lWm%H%YqnPmR5iR(OC-^#d_o;DjA4^BfO~wJOg83SjXnKW~MWL0m6be#} zyh8FM>AU!fsJhG7;0rOa29K+lkX;N`I)7sE10P&SCXzE)fujs6hZ)?**F^>!+@&kk z%<_?2`HKbd73M4q%^Y>;uv$12UlnPnczS?`Tu*vhI_YU`s;6)8h#C@n%+o{#b59TG g>ER4d`KT10q)slr&DQU-$&g9;p6ADO$qBptA2WVYKmY&$ diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entity/Hand.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/Hand.class index a0bf078f69d209d836ef945ada1352fed7b7f01d..bc00d4d8c71418737ac657f0c9d4c47847355a9b 100644 GIT binary patch delta 24 fcmcb@afM@o1q+LhtJ_3dJyzBrPj`=v`A?YvUNi@o delta 24 fcmcb@afM@o1uJWir@O~Q8$A{lA6K`H`A?YvUiJr< diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entity/User.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/User.class index e3797ea56cd22ff10561c9f91ef9aa77f44d430e..a02bda9088c3304ba319db17097aa563341e8e6a 100644 GIT binary patch literal 2866 zcmaJ?+fo}x5IqCB0ZX{c77z>$cEs99iE}qLHh0Ux;DD@D{E*aw#;}gGa;;Xb;s^3Q zNmW#F7$cN@o6ZPW5}nXbOaCk7UAMqeE|#ivTZ}#0(T*l%U9HF~&@9uWwtN}} zcC814ET6seJxZHfd*xF3C%#SW9UF~WtL_jx;aGLboN-#UnnmjU%qb|$$9io!ExxW$ zqi51j5_zGx_{20MlR7LPl=gOb*G8ktyGC|xNAI=jN7^mRm7=N0Ua?$T_Tk5bcWP6< z-U%SdMIOK9N#H!=!xUb*K5OC}e2mV73C#R&op}-Q4^v>N!oqu@z0*eZrR7pmk8e$l zPPA95Doo|$s)*;!;5-`_gJ7l!Ac@WwA;h(%Kx3i>#tZrDZ3Q|-=rt+K6y5yA5WlQn z)KK=EbEhZ@L$tp%yAe|Fw2}*q2pr37rUS22P#Q--X*?T6?X(r>457DzdYn9m^*i20 zMP`T>T1-X7jl9?3VWZ_9>up=)9gu7)ND}lanJvGiG2v$T{8tOw&sr&q7J-^rU@Plm zDWzCUY;PRza(4^;_>^;=`y~w%_i&$P%T3x8UZco0n_p-=%=t6^KG%4FFSuZ@4Ua}S zhmK=`V?ui1)zjJYQ+B$qz`k zG5iNV^x-Rx<4AD4%W;}6GcdURnqQ2-3E$vb?ga2VdI;bL&S}ntD4=<-jS2I98(DLi z_+~EA# zzjG0&C3yk9>!m`;3xytIHB?AuKZ|@-E+dz}e9%{X1R)z(T|_3-Bv*uIQW8)&J;76s z-JG)BoF2wZy_o6|uo zr|+>F$7!ycQ-z#z{*9qnndZwm}{pdws2K^QWU?~jki|5@ySG2E?`D9lc`o)&Q z=v01s<$%O&AHKkm431hD!ZC$2C9jqXeA}&Wd45g%^_+GCC)mlos*AZEyP_3_W9(!w z!VP1;5jfS{yzkpPD^5M=$1r;2loJ+CVoafrM|^Aqv`r5~OGZP44bEu`XM`o;ltu5H zg>jr$Ncvutho4*NhPcLkW8g~*7jRLbchm8M_hoxWVI({#aB4cY*zoPZ@mwQ&$--q! zk!n}=hQe5j@QPQmtBZD^3sf~4SqoQiRUxHowo_F&))I~Nz<1orEKRI67`K`(I(28W zs^?wT3(TQ(`Xm)T*nVK=J~9aOTDv8k%a^HkF?Q>Tb~TZ8xhTn`SZ|#vVutD1p&E3u zu=CMqo%%Yo9I0(O(>j?)mm~JED}MZX3Er> zWGK!*5shHd>xH%a>JtWi-Kn^C(C~TSO=_{)_e$Y@*E}yo^)oxiL*u!g+wM-mlaV6h zu{_J3;}*n0N?0{YZUo+jJQW7Exfce6>54x=)c0bRY4CPzIAs=t<9=dN_5L<34D43C z5lJq1W%_$$#c}m&qqeDigCT_^wN|u!N3f!o$>6<16Wyy`E+$?0Wh#uH=@!xTpY0k# zb5-ILOb*&_NhoICwO@u7{iyDzi@eg$b@1LRzqEbUQFzN!6&>Umoa0k*9f*Zf5BEpo zd=Q%%8W3bAgC-8W{}iURP7}p&XUC6(2l>3i-(8Iv+~b7bsc7pcpONBFsF;+-e{lE0 z?k-#X!QIQ@u9CJM?G6~rY)W;$OvrbO<1CRAxNlsz#@Pd|=jaNBzQx10P!BOrJuV+% zp>?^)D6qt(%yE=jV0e-YN1D;J zNO*$lCwyy#Ht-aAuEhQ_p~U__w4dQ~ri=aTl_pMQuQqWudreLrOExi)9ZNManH@_v zF})9PoqsKWn;ihRn5f&txr0l%OQabh-Q#T5fISTlkHCl#rCw%|S1E`YlScWr%$SrV z%}mlG4$qiWr16aNu{71CWYQwda|jNihm%N-A8KUgA5BolIvtnI6Y7 ztzn&Kh;kd#*-oZqGCk|U6yBYPN#dqZh>g==C(~*y)2sM64R$iEk!igPQ+R13Cds2h z5#PteX`+*<7|ZknUdP30qLb+jncj9`3R^|QBz2|mhQbGPH`&SbE|%#>{1nGD*~zp? zrcxKCut7ykQs)Zquo1&F-O02S%k(qsIHr~x9@h9anLc!33fp4DbcIX_Y?`rTTHz6{ M%Uo+xviEKM4=6;JlK=n! diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/impl/ComputerService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/impl/ComputerService.class index fb2d6632d0105fe75fb798314f7a334d67257529..7e1040f92c2af8247f7b6d197308178751e48872 100644 GIT binary patch delta 685 zcmZ`$yG{a86r8)f;x4T50SfX)-Ca;oToHK&6FaP|El7L>LP%7wvEU=_7uZ@!u)%^* z+xbJpv+D+ogzDTmnL9Ig?!@!9EPhQsM*vEw^u;(+Tz8H3O|{ixSXH%Jv;JI9cRP(% zy4HB=6eP&lbi@w*5TL=sk;eth$ zt{V64dZ+t%%aENDFY;G=TqqZK(-vWuyv)?W$ac*VJ75qX!A(=4piY{qH613>c-i4o zlGYCJAVp_`4>+)geVPm_I3OL3zc_+DgDVmO)@gc8zR$#x#Roa2aY*-$3>8Me5b?sG zX>`^J83tnV1Fj*~7~Tm9n8on_i6L6D8I#jHrmIvUY^GF&t{GV*Ln<}IhA^iUUJy~o z2}xNIb<+4X$kH-|(*^oly?KMg0)rgRW(K*bL7tW|6s#`Jtp~`~10n*(Nc`_Zomx;( TdD^o)-?M z*^f|g@I$yb6n}SwL@b%!y}S2&@7~?R)M(QD7`?s$I7Iu&4461$9V2g}fTFl#ZKGtv z!-5#HM#g9G#aG5RvIrPj;)fRpC;XU+q2`$rr@f2I?)A;p`QTf-W#uI>a9FSj%Vcj5 zE)(j6x?-WY*WJoWbOusZXf5?pwTd-DhG|sEClsH0sV}uAF(5}cqx7!gR-^|d*04@< z8`6alktVZnp&_Johvd@0sy-uIdqIxlktj!Uk9>-6y^qbrxK2}BYI2)43uvSS%#8Oc%O9#L&S8XxU diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/impl/UserService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/impl/UserService.class index 9739d4b540cb8ad1a89a67e38bf3afab1f0ad924..7931de740cce05ea390f2da91349c1e46bfcf7e0 100644 GIT binary patch delta 1151 zcmZ`&+fEZv6kTVgou-|!Q49*rD8&nxf)*c)#)ttQYjGFn`(ysyMJ>(yGkSbbMdn+T)NLO&uhB4!NR!YQ1VWie+&Eezs}{3?pZ zISc18B=<$l7_l&ln0%wHr;I5JNnDl-dct>Az?L6GOm^+u(>`Szti;y*F+i!m(!`fO21R6Ll@Qs;QhV79NYQfGqJ?1SQ#T&2nvikzgZ5!cmrqk4u%PbA=>P7$7120 zL!bt{#5c_`C8)6s^e(x4%jDzYDn7YxdM!?NQ%>P4xZf7O;sQNjk!$Y&?U`PSQ-=Vp z;$a(T)dhOQ;v^m`Al43A9PhL^+YMh?yyjhoHJ9KC36gk9OB8|Hb~VFrYlfFJ{KA!k aOPcp^gk6)vox88$8NoChdG7v}W#AuRS9EXy delta 1118 zcmZ{jO-~b16o%h>FCA<h%%#+7@2iQ%2;I71j`b?!NH?)%*HF|Ewo-o($pU%vskj?K@BE}<7i z(KuEhWX%{iF@XzuO3fU* zXkrpm0v#`{-MV0ENoDjqmFk!l$m$QVtm?#-y&q~t3@fX_jPBZ&wQJ>{Tg^tk+T5u( z>f5sd@%3kornU2GyT11#_E8O9hJZv8Chb+e*r2^eo1l%kUO|`Q>5&=l8*mh}JUjVd z#vE$|&&N@uPC>tpXKg1pK@UjN_PF_J*H3!+AaNcg-dk`~1RY1@1TQEQc%EmvBaqB} zLFZTt-4dVZN(Ardi_k6GxDg1KdF!TobCE#~AOg@61`I?1D!3KsSA6~3405Va_J1qr z3j>BC097mnfT|C;!{8a5^$J*`Rs3eU9U5=pwDMZy_TQN!kaGzG?L@!#CTcFhnc^}} z(w#zFG7JX(HEe|v+)0mjcp`)0dh+dh?Dph*ToS9e8x*wa1MV@%O^*PKgaOkL4O_!{ zpucAOfcuP2;6VgnEDR__05%S;)P}GBkouE&#Cn~bEz>$XyP5KqHZSoVmk#f3@c<*` UHz^hVA0!@AP2!3FV?G7;0j7Iv+W-In diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IComputerService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IComputerService.class index c4ec059119ab11872777a6618fe6b34267a3f910..76fca87488781535108777d776240bdfd289fea4 100644 GIT binary patch delta 207 zcmdnXx|EIU)W2Q(7#J9A86+oiEwp4$%gjr0%*kP75Yo`}$x19s)GsZ`%+dGBEH1HT zWZ(?WOwUU!DJ@E6WH3WfFjjenvT%UM|%8CV(E7#JDYfhMyuGH@_30EIXi zxPU_3Kp`du9w48Wfe*-nD&YqTL!<ln78tltB!|U?4{vY@`GLkscmW delta 128 zcmZ3=wwIOb)W2Q(7#J9A8N?=XEu75H=*r6(oSB}NSW;S)%E;h5S&>m=b1kDGBO}Y? z^-NMeObqNm)vSyR+(3#4h?y98Sr{4k82EuKsFVO)N)RX|#30PT2$SLfN-;8sfc1$2 E0AN-SH2?qr diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IUserService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IUserService.class index b1e338f7c2f112ec7fc6ed9c537eb50be472b244..065e15ebfab21a67055af2d44f62e72c3bc16ce3 100644 GIT binary patch delta 336 zcmZqTea6dm>ff$?3=9mm40;o}rdqP6W#*+g=HxIk2x(~gWF?j*>X(*e=IHxm7MEBv zGH?cGrspM=loq8jG8myKn4Hg|K5@Piqu0dsnv>5kD{r=93}a#BnmmzJs-BU78)yR) z0}l%$11|#~ki`zff$?3=9mm4B8X9rcV6t#K{?)nVy$eQd+dxj&T`FJtqqz11kd?10w@F zP#G&D0|x^GP>7R(i-8Fy#0`|;0b(WwUZ4~o13yAa04^m6loDbPMo5Xkr9^>JVhrL? zDNZH^380iDSh^TwsuWOOnn4DkRTi#Q4k#thpn#B4gi9#_rIZ;|5K^jeDK(&!I)es^ Nr+^$ypaLcaEdUN^61xBZ diff --git a/besser/generators/spring/endpoint.http b/besser/generators/spring/endpoint.http new file mode 100644 index 000000000..ad9763297 --- /dev/null +++ b/besser/generators/spring/endpoint.http @@ -0,0 +1,27 @@ +### GET ALL +GET http://localhost:8080/api/computer +Accept: application/json + +### GET BY ID +GET http://localhost:8080/api/computer/1 +Accept: application/json + +### CREATE +POST http://localhost:8080/api/computer +Content-Type: application/json + +{ + "model": "MacBook Pro22" +} + +### UPDATE +PUT http://localhost:8080/api/computer/1 +Content-Type: application/json + +{ + "model": "ThinkPad X1" +} + +### DELETE +DELETE http://localhost:8080/api/computer/1 +Accept: application/json \ No newline at end of file diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index 7660feab1..c10b53340 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -58,12 +58,12 @@ def proba_jednostavna_klasa(): # Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True) # }) - assoc = BinaryAssociation(name="ManyToMany", ends={ - Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), - Property(name="owners", type=user, multiplicity=Multiplicity(0, "*"), is_navigable=True) - }) + # assoc = BinaryAssociation(name="ManyToMany", ends={ + # Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), + # Property(name="owners", type=user, multiplicity=Multiplicity(0, "*"), is_navigable=True) + # }) - model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}, associations={assoc}) + model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}, associations={}) # generator = SpringEntityGenerator( # model, diff --git a/besser/generators/spring/spring_backend_generator.py b/besser/generators/spring/spring_backend_generator.py index d670ec7ee..665a3a584 100644 --- a/besser/generators/spring/spring_backend_generator.py +++ b/besser/generators/spring/spring_backend_generator.py @@ -5,7 +5,9 @@ from besser.BUML.metamodel.structural.structural import DomainModel from besser.generators.generator_interface import GeneratorInterface +from besser.generators.spring.spring_controller_generator import SpringControllerGenerator from besser.generators.spring.spring_entity_generator import SpringEntityGenerator +from besser.generators.spring.spring_http_generator import SpringHttpGenerator from besser.generators.spring.spring_repository_generator import SpringRepositoryGenerator from besser.generators.spring.spring_service_generator import SpringServiceGenerator @@ -38,6 +40,8 @@ def generate(self): self._generate_entities() self._generate_repositories() self._generate_services() + self._generate_controllers() + self._generate_http() def _generate_pom_file(self): file_path = self.build_generation_path(file_name="pom.xml") @@ -158,4 +162,23 @@ def _generate_services(self): package_name=f"{self.package_name}.service" ) + generator.generate() + + def _generate_controllers(self): + generator = SpringControllerGenerator( + self.model, + f"{self.package_name}.entity", + f"{self.package_name}.service", + output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "controller", + package_name=f"{self.package_name}.controller" + ) + + generator.generate() + + def _generate_http(self): + generator = SpringHttpGenerator( + self.model, + output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "http_test" + ) + generator.generate() \ No newline at end of file diff --git a/besser/generators/spring/spring_controller_generator.py b/besser/generators/spring/spring_controller_generator.py new file mode 100644 index 000000000..8b0b34e57 --- /dev/null +++ b/besser/generators/spring/spring_controller_generator.py @@ -0,0 +1,61 @@ +import os +from pathlib import Path +from typing import List + +from jinja2 import Environment, FileSystemLoader + +from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, StringType, TimeDeltaType, TimeType +from besser.generators.generator_interface import GeneratorInterface + +class SpringControllerGenerator(GeneratorInterface): + + def __init__(self, model: DomainModel, + entity_package_name: str, + service_package_name: str, + output_dir: str = "./generated/controller", + package_name: str = "com.example.controller"): + super().__init__(model, output_dir) + + self.package_name: str = package_name + self.entity_package_name: str = entity_package_name + self.service_package_name: str = service_package_name + self.enumerations: set[Enumeration] = model.get_enumerations() + self.classes: set[Class] = model.classes_sorted_by_inheritance() + + def generate(self): + for cls in self.classes: + if not cls.is_abstract: + self._generate_controller_file(cls) + + def _generate_controller_file(self, cls: Class): + file_path = self.build_generation_path(file_name=f"{cls.name.capitalize()}Controller.java") + Path(file_path).parent.mkdir(parents=True, exist_ok=True) + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) + repository_template = env.get_template("controller.java.j2") + + imports: set[str] = set() + imports.add("org.springframework.beans.factory.annotation.Autowired") + imports.add("org.springframework.web.bind.annotation.RestController") + imports.add("org.springframework.web.bind.annotation.RequestMapping") + imports.add("org.springframework.web.bind.annotation.PathVariable") + imports.add("org.springframework.web.bind.annotation.RequestBody") + imports.add("org.springframework.web.bind.annotation.GetMapping") + imports.add("org.springframework.web.bind.annotation.PostMapping") + imports.add("org.springframework.web.bind.annotation.PutMapping") + imports.add("org.springframework.web.bind.annotation.DeleteMapping") + imports.add("org.springframework.http.ResponseEntity") + imports.add("java.util.Optional") + imports.add("java.util.List") + imports.add(f"{self.service_package_name}.interfaces.I{cls.name.capitalize()}Service") + imports.add(f"{self.entity_package_name}.{cls.name}") + + context = { + "package": f"{self.package_name}", + "imports": sorted(imports), + "cls": cls.name + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = repository_template.render(**context) + f.write(generated_code) diff --git a/besser/generators/spring/spring_http_generator.py b/besser/generators/spring/spring_http_generator.py new file mode 100644 index 000000000..6789b45f4 --- /dev/null +++ b/besser/generators/spring/spring_http_generator.py @@ -0,0 +1,36 @@ +import os +from pathlib import Path +from typing import List + +from jinja2 import Environment, FileSystemLoader + +from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, StringType, TimeDeltaType, TimeType +from besser.generators.generator_interface import GeneratorInterface + +class SpringHttpGenerator(GeneratorInterface): + + def __init__(self, model: DomainModel, + output_dir: str = "./generated/http"): + super().__init__(model, output_dir) + + self.enumerations: set[Enumeration] = model.get_enumerations() + self.classes: set[Class] = model.classes_sorted_by_inheritance() + + def generate(self): + for cls in self.classes: + if not cls.is_abstract: + self._generate_http_file(cls) + + def _generate_http_file(self, cls: Class): + file_path = self.build_generation_path(file_name=f"{cls.name[0].lower() + cls.name[1:]}.http") + templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) + http_template = env.get_template("http.http.j2") + + context = { + "cls": cls.name + } + + with open(file_path, mode="w", encoding="utf-8") as f: + generated_code = http_template.render(**context) + f.write(generated_code) diff --git a/besser/generators/spring/spring_service_generator.py b/besser/generators/spring/spring_service_generator.py index 90d8f90a7..2376b4994 100644 --- a/besser/generators/spring/spring_service_generator.py +++ b/besser/generators/spring/spring_service_generator.py @@ -48,6 +48,7 @@ def _generate_service_files(self, cls: Class): imports: set[str] = set() imports.add(f"{self.entity_package_name}.{cls.name}") + imports.add("java.util.List") if cls.attributes: imports.add("java.util.ArrayList") @@ -121,7 +122,7 @@ def _generate_service_files(self, cls: Class): for method in context["methods"]: parameter: str = method["parameter"] tokens: List[str] = parameter.split(", ") - method["parameter_names"] = ", ".join(token.split(" ")[1] for token in tokens) + method["parameter_names"] = ", ".join(token.split(" ")[1] for token in tokens) if parameter else "" with open(file_path, mode="w", encoding="utf-8") as f: generated_code = service_template.render(**context) @@ -129,6 +130,11 @@ def _generate_service_files(self, cls: Class): def _get_crud_methods(self, cls: Class) -> List[object]: return [ + { + "return_value": f"List<{cls.name}>", + "name": "findAll", + "parameter": "" + }, { "return_value": f"Optional<{cls.name}>", "name": "findById", diff --git a/besser/generators/spring/templates/controller.java.j2 b/besser/generators/spring/templates/controller.java.j2 new file mode 100644 index 000000000..0b58f21f8 --- /dev/null +++ b/besser/generators/spring/templates/controller.java.j2 @@ -0,0 +1,59 @@ +package {{ package }}; + +{% for import in imports %} +import {{ import }}; +{% endfor %} + +@RestController +@RequestMapping("/api/{{ cls[0] | lower + cls[1:] }}") +public class {{ cls }}Controller { + + @Autowired + private I{{ cls }}Service _{{ cls[0] | lower + cls[1:] }}Service; + + @GetMapping + public ResponseEntity> getAll() { + return ResponseEntity.ok(_{{ cls[0] | lower + cls[1:] }}Service.findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity<{{ cls }}> getById(@PathVariable Integer id) { + return _{{ cls[0] | lower + cls[1:] }}Service.findById(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity<{{ cls }}> create(@RequestBody {{ cls }} entity) { + {{ cls }} saved = _{{ cls[0] | lower + cls[1:] }}Service.save(entity); + return ResponseEntity.ok(saved); + } + + @PutMapping("/{id}") + public ResponseEntity<{{ cls }}> update(@PathVariable Integer id, @RequestBody {{ cls }} entity) { + + Optional<{{ cls }}> existing = _{{ cls[0] | lower + cls[1:] }}Service.findById(id); + + if (existing.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + entity.setId(id); + {{ cls }} updated = _{{ cls[0] | lower + cls[1:] }}Service.save(entity); + + return ResponseEntity.ok(updated); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteById(@PathVariable Integer id) { + + Optional<{{ cls }}> existing = _{{ cls[0] | lower + cls[1:] }}Service.findById(id); + + if (existing.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + _{{ cls[0] | lower + cls[1:] }}Service.delete(existing.get()); + + return ResponseEntity.noContent().build(); + } + +} \ No newline at end of file diff --git a/besser/generators/spring/templates/http.http.j2 b/besser/generators/spring/templates/http.http.j2 new file mode 100644 index 000000000..ce3a8750c --- /dev/null +++ b/besser/generators/spring/templates/http.http.j2 @@ -0,0 +1,27 @@ +### GET ALL +GET http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }} +Accept: application/json + +### GET BY ID +GET http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }}/ +Accept: application/json + +### CREATE +POST http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }} +Content-Type: application/json + +{ + +} + +### UPDATE +PUT http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }}/ +Content-Type: application/json + +{ + +} + +### DELETE +DELETE http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }}/ +Accept: application/json \ No newline at end of file From 1e95661a4e080a2591654beffbd5899415322814 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Sat, 14 Mar 2026 18:28:39 +0100 Subject: [PATCH 10/16] add generator to api, fix path chaining --- besser/generators/spring/proba.py | 39 ++++++++++-- .../spring/spring_backend_generator.py | 6 +- .../web_modeling_editor/backend/backend.py | 59 +++++++++++++++++++ .../backend/config/generators.py | 8 +++ 4 files changed, 104 insertions(+), 8 deletions(-) diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index c10b53340..a6cfb377a 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -11,6 +11,7 @@ ) from besser.generators.spring.spring_entity_generator import SpringEntityGenerator from besser.generators.spring.spring_backend_generator import SpringBackendGenerator +from besser.utilities.web_modeling_editor.backend.services.converters.buml_to_json.class_diagram_converter import class_buml_to_json def proba_jednostavna_klasa(): current_dir = Path(__file__).parent @@ -32,7 +33,7 @@ def proba_jednostavna_klasa(): user.add_attribute(Property(name="birthday", type=TimeDeltaType, visibility="protected")) user.add_attribute(Property(name="birthday2", type=DateTimeType, visibility="protected")) # user.add_attribute(Property(name="hand", type=hand)) - # user.add_attribute(Property(name="nicknames", type=DateTimeType, multiplicity=Multiplicity(0, "*"), default_value="1.2f, 3.2f")) + user.add_attribute(Property(name="nicknames", type=FloatType, multiplicity=Multiplicity(0, "*"), default_value="1.2f, 3.2f")) # user.add_attribute(Property(name="address", type=address, visibility="protected")) thing = Class(name="Thing", is_abstract=True) @@ -48,10 +49,10 @@ def proba_jednostavna_klasa(): # Property(name="owner", type=user, multiplicity=Multiplicity(1, 1), is_navigable=False) # }) - # assoc = BinaryAssociation(name="onetomany", ends={ - # Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True), - # Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=False) - # }) + assoc = BinaryAssociation(name="onetomany", ends={ + Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True), + Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=False) + }) # assoc2 = BinaryAssociation(name="manytoone", ends={ # Property(name="computerss", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), @@ -77,5 +78,33 @@ def proba_jednostavna_klasa(): files = generator.generate() + json_model = class_buml_to_json(model) + + import requests + + res = requests.post("http://localhost:9000/besser_api/generate-output", json={ + "title": "spring backendaa", + "config": { + "spring_boot_version": "3.5.11", + "java_version": "17", + "app_name": "Transformers", + "package_name": "com.transformers", + "project_name": "trans" + }, + "model": { + "elements": json_model["elements"], + "relationships": json_model["relationships"] + }, + "generator": "spring" + }) + + # if res.status_code == 200: + # with open("project.zip", "wb") as f: + # f.write(res.content) + + # print("ZIP sacuvan kao project.zip") + # else: + # print(res.status_code, res.text) + if __name__ == "__main__": proba_jednostavna_klasa() \ No newline at end of file diff --git a/besser/generators/spring/spring_backend_generator.py b/besser/generators/spring/spring_backend_generator.py index 665a3a584..b5eb6bc52 100644 --- a/besser/generators/spring/spring_backend_generator.py +++ b/besser/generators/spring/spring_backend_generator.py @@ -65,7 +65,7 @@ def _generate_mvn_files(self): templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") env = Environment(loader=FileSystemLoader(templates_path)) - file_path = self.build_generation_path(file_name=self.output_dir / ".mvn" / "wrapper" / "maven-wrapper.properties") + file_path = self.build_generation_path(file_name=Path(self.output_dir, ".mvn", "wrapper", "maven-wrapper.properties")) Path(file_path).parent.mkdir(parents=True, exist_ok=True) maven_wrapper_template = env.get_template("maven-wrapper.properties.j2") @@ -73,7 +73,7 @@ def _generate_mvn_files(self): generated_code = maven_wrapper_template.render() f.write(generated_code) - file_path = self.build_generation_path(file_name=self.output_dir / ".mvn" / "mvnw") + file_path = self.build_generation_path(file_name=Path(self.output_dir, ".mvn", "mvnw")) Path(file_path).parent.mkdir(parents=True, exist_ok=True) mvnw_template = env.get_template("mvnw.j2") @@ -81,7 +81,7 @@ def _generate_mvn_files(self): generated_code = mvnw_template.render() f.write(generated_code) - file_path = self.build_generation_path(file_name=self.output_dir / ".mvn" / "mvnw.cmd") + file_path = self.build_generation_path(file_name=Path(self.output_dir, ".mvn", "mvnw.cmd")) Path(file_path).parent.mkdir(parents=True, exist_ok=True) mvnw_cmd_template = env.get_template("mvnw.cmd.j2") diff --git a/besser/utilities/web_modeling_editor/backend/backend.py b/besser/utilities/web_modeling_editor/backend/backend.py index 32601dbca..e95aa7077 100644 --- a/besser/utilities/web_modeling_editor/backend/backend.py +++ b/besser/utilities/web_modeling_editor/backend/backend.py @@ -569,6 +569,8 @@ async def _handle_class_diagram_generation( # Generate based on generator type if generator_type == "django": return await _generate_django(buml_model, generator_class, config, temp_dir) + if generator_type == "spring": + return await _generate_spring(buml_model, generator_class, config, temp_dir) if generator_type == "sql": return await _generate_sql(buml_model, generator_class, config, temp_dir) if generator_type == "sqlalchemy": @@ -964,6 +966,63 @@ async def _generate_django(buml_model, generator_class, config: dict, temp_dir: os.chdir(original_cwd) cleanup_temp_resources(temp_dir) +async def _generate_spring(buml_model, generator_class, config: dict, temp_dir: str): + """Generate Spring project.""" + + if not config: + raise HTTPException(status_code=400, detail="Spring configuration is required") + + project_dir = os.path.join("D:\\Faks\\Master\\Master rad\\BESSER-master\\besser\\utilities\\web_modeling_editor\\backend", config["project_name"]) + print(project_dir) + # Clean up any existing project directory + if os.path.exists(project_dir): + shutil.rmtree(project_dir) + + os.makedirs(temp_dir, exist_ok=True) + original_cwd = os.getcwd() + os.chdir(temp_dir) + + try: + print(config) + generator_instance = generator_class( + model=buml_model, + spring_boot_version=config["spring_boot_version"], + java_version="17", + app_name=config["app_name"], + package_name=config["package_name"], + output_dir=project_dir, + ) + print("posle") + generator_instance.generate() + + # Wait for file system operations + await asyncio.sleep(1) + + # Validate generation + if not os.path.exists(project_dir) or not os.listdir(project_dir): + raise ValueError("Spring project generation failed: Output directory is empty") + + # Create ZIP file + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: + for root, _, files in os.walk(project_dir): + for file in files: + file_path = os.path.join(root, file) + arc_name = os.path.relpath(file_path, project_dir) + zip_file.write(file_path, arc_name) + + zip_buffer.seek(0) + file_name = get_filename_for_generator("spring") + + return StreamingResponse( + zip_buffer, + media_type="application/zip", + headers={"Content-Disposition": f"attachment; filename={file_name}"}, + ) + finally: + os.chdir(original_cwd) + # cleanup_temp_resources(temp_dir) + async def _generate_sql(buml_model, generator_class, config: dict, temp_dir: str): """Generate SQL files.""" diff --git a/besser/utilities/web_modeling_editor/backend/config/generators.py b/besser/utilities/web_modeling_editor/backend/config/generators.py index cb8e8cca5..746d218d4 100644 --- a/besser/utilities/web_modeling_editor/backend/config/generators.py +++ b/besser/utilities/web_modeling_editor/backend/config/generators.py @@ -7,6 +7,7 @@ from besser.generators.python_classes import PythonGenerator from besser.generators.java_classes import JavaGenerator from besser.generators.pydantic_classes import PydanticGenerator +from besser.generators.spring.spring_backend_generator import SpringBackendGenerator from besser.generators.sql_alchemy import SQLAlchemyGenerator from besser.generators.sql import SQLGenerator from besser.generators.backend import BackendGenerator @@ -58,6 +59,13 @@ class GeneratorInfo(NamedTuple): category="web_framework", requires_class_diagram=True ), + "spring": GeneratorInfo( + generator_class=SpringBackendGenerator, + output_type="zip", + file_extension=".zip", + category="web_framework", + requires_class_diagram=True + ), "backend": GeneratorInfo( generator_class=BackendGenerator, output_type="zip", From b12dd63e51813ffb55441d2c168fb40c99603042 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Mon, 16 Mar 2026 13:51:03 +0100 Subject: [PATCH 11/16] entity methods --- besser/generators/spring/proba.py | 55 +++++++---- .../spring/spring_entity_generator.py | 97 ++++++++++++++++++- .../spring/templates/entity.java.j2 | 9 ++ .../web_modeling_editor/backend/backend.py | 9 +- 4 files changed, 142 insertions(+), 28 deletions(-) diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index a6cfb377a..15e97bb49 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -2,12 +2,13 @@ Jednostavna proba Spring Entity Generatora - sa apsolutnom putanjom """ +from datetime import datetime import sys import os from pathlib import Path from besser.BUML.metamodel.structural.structural import ( - BinaryAssociation, DateTimeType, DateType, DomainModel, Class, FloatType, Generalization, IntegerType, Multiplicity, Property, StringType, TimeDeltaType, Type, Association, Enumeration, EnumerationLiteral + BinaryAssociation, DateTimeType, DateType, DomainModel, Class, FloatType, Generalization, IntegerType, Method, Multiplicity, Parameter, Property, StringType, TimeDeltaType, TimeType, Type, Association, Enumeration, EnumerationLiteral ) from besser.generators.spring.spring_entity_generator import SpringEntityGenerator from besser.generators.spring.spring_backend_generator import SpringBackendGenerator @@ -26,12 +27,19 @@ def proba_jednostavna_klasa(): user = Class(name="User") user.add_attribute(Property(name="id", type=IntegerType, is_id=True)) - user.add_attribute(Property(name="email", type=StringType, is_optional=True)) + user.add_attribute(Property(name="email", type=StringType, is_optional=True, default_value="mikica")) # user.add_attribute(Property(name="firstName", type=StringType, is_optional=True, default_value="John")) user.add_attribute(Property(name="age", type=IntegerType, default_value=2)) - user.add_attribute(Property(name="role", type=role, visibility="private")) - user.add_attribute(Property(name="birthday", type=TimeDeltaType, visibility="protected")) - user.add_attribute(Property(name="birthday2", type=DateTimeType, visibility="protected")) + user.add_attribute(Property(name="role", type=role, visibility="private", default_value="ADMIN")) + user.add_attribute(Property(name="birthday", type=DateType, visibility="protected", default_value={ + "year": 2056, + "month": 10, + "day": 30, + "hour": 16, + "minute": 30, + "second": 27 + })) + user.add_attribute(Property(name="birthday2", type=TimeType, visibility="protected")) # user.add_attribute(Property(name="hand", type=hand)) user.add_attribute(Property(name="nicknames", type=FloatType, multiplicity=Multiplicity(0, "*"), default_value="1.2f, 3.2f")) # user.add_attribute(Property(name="address", type=address, visibility="protected")) @@ -41,9 +49,20 @@ def proba_jednostavna_klasa(): computer = Class(name="Computer") computer.add_attribute(Property(name="id", type=IntegerType, is_id=True)) computer.add_attribute(Property(name="model", type=StringType)) + + method1 = Method("destroyComputer", visibility="private", parameters=[ + Parameter("when", DateTimeType), + Parameter("how", StringType) + ]) + method2 = Method("makeComputer", visibility="private", parameters=[ + Parameter("when", role) + ], type=DateTimeType) + computer.add_method(method1) + computer.add_method(method2) gen1 = Generalization(general=thing, specific=computer) - + # a = datetime(2026, 11, 20, 16, 13, 22) + # a. # assoc = BinaryAssociation(name="onetoone", ends={ # Property(name="computer", type=computer, multiplicity=Multiplicity(1, 1), is_navigable=True), # Property(name="owner", type=user, multiplicity=Multiplicity(1, 1), is_navigable=False) @@ -51,7 +70,7 @@ def proba_jednostavna_klasa(): assoc = BinaryAssociation(name="onetomany", ends={ Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True), - Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=False) + Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True) }) # assoc2 = BinaryAssociation(name="manytoone", ends={ @@ -64,7 +83,7 @@ def proba_jednostavna_klasa(): # Property(name="owners", type=user, multiplicity=Multiplicity(0, "*"), is_navigable=True) # }) - model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}, associations={}) + model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}, associations={assoc}) # generator = SpringEntityGenerator( # model, @@ -72,11 +91,11 @@ def proba_jednostavna_klasa(): # package_name="spring.entities" # ) - generator = SpringBackendGenerator( - model, "3.5.11", java_version="17", app_name="Transformers", output_dir=current_dir / "backend", package_name="com.transformers" - ) + # generator = SpringBackendGenerator( + # model, "3.5.11", java_version="17", app_name="Transformers", output_dir=current_dir / "backend", package_name="com.transformers" + # ) - files = generator.generate() + # files = generator.generate() json_model = class_buml_to_json(model) @@ -98,13 +117,13 @@ def proba_jednostavna_klasa(): "generator": "spring" }) - # if res.status_code == 200: - # with open("project.zip", "wb") as f: - # f.write(res.content) + if res.status_code == 200: + with open("project.zip", "wb") as f: + f.write(res.content) - # print("ZIP sacuvan kao project.zip") - # else: - # print(res.status_code, res.text) + print("ZIP sacuvan kao project.zip") + else: + print(res.status_code, res.text) if __name__ == "__main__": proba_jednostavna_klasa() \ No newline at end of file diff --git a/besser/generators/spring/spring_entity_generator.py b/besser/generators/spring/spring_entity_generator.py index cc8effc28..1d6516339 100644 --- a/besser/generators/spring/spring_entity_generator.py +++ b/besser/generators/spring/spring_entity_generator.py @@ -4,7 +4,7 @@ from typing import List from jinja2 import Environment, FileSystemLoader from besser.BUML.metamodel.structural import DomainModel -from besser.BUML.metamodel.structural.structural import BinaryAssociation, Class, Enumeration, Property, StringType, BooleanType, DateTimeType, DateType, FloatType, IntegerType, TimeDeltaType, TimeType +from besser.BUML.metamodel.structural.structural import BinaryAssociation, Class, Enumeration, Parameter, Property, StringType, BooleanType, DateTimeType, DateType, FloatType, IntegerType, TimeDeltaType, TimeType from besser.generators import GeneratorInterface class SpringEntityGenerator(GeneratorInterface): @@ -16,7 +16,7 @@ class SpringEntityGenerator(GeneratorInterface): FloatType.name: "Float", DateType.name: "LocalDate", DateTimeType.name: "LocalDateTime", - TimeType.name: "LocalDateTime", + TimeType.name: "LocalTime", TimeDeltaType.name: "Duration" } @@ -66,7 +66,8 @@ def _generate_class_file(self, cls: Class, assocs_for_class: List[BinaryAssociat self._prepare_attributes(cls), key=lambda a: (not a["is_id"], not a["is_enum"], not a["is_list"], a["name"]) ), - "relations": relations, + "methods": self._prepare_methods(cls), + "relations": self._prepare_relations(cls, assocs_for_class), } with open(file_path, mode="w", encoding="utf-8") as f: @@ -89,6 +90,24 @@ def _prepare_attributes(self, cls: Class) -> List[object]: if is_list: attr_type = f"List<{attr_type}>" + default_value: str = "" + + if attr.default_value: + if attr.type.name == DateType.name: + default_value = f"""LocalDate.of{attr.default_value["year"], attr.default_value["month"], attr.default_value["day"]}""" + elif attr.type.name == DateTimeType.name: + default_value = f"""LocalDateTime.of{attr.default_value["year"], attr.default_value["month"], attr.default_value["day"], attr.default_value["hour"], attr.default_value["minute"], attr.default_value["second"]}""" + elif attr.type.name == TimeType.name: + default_value = f"""LocalTime.of{attr.default_value["hour"], attr.default_value["minute"], attr.default_value["second"]}""" + elif attr.type.name == TimeDeltaType.name: + default_value = f"Duration.ofSeconds({attr.default_value})" + elif attr.type.name == "str": + default_value = f"\"{attr.default_value}\"" + elif is_enum: + default_value = f"{attr.type.name}.{attr.default_value}" + else: + default_value = attr.default_value + attributes.append({ "is_id": attr.is_id, "column_name": self._to_snake_case(attr.name), @@ -98,11 +117,54 @@ def _prepare_attributes(self, cls: Class) -> List[object]: "visibility": attr.visibility, "type": attr_type, "name": attr.name, - "default_value": f"\"{attr.default_value}\"" if attr.default_value and attr.type.name == "str" and not is_list else attr.default_value + "default_value": default_value }) return attributes + def _prepare_methods(self, cls: Class) -> List[object]: + methods: List[object] = [] + + for method in cls.methods: + method_return_type: str = "" + + if not method.type: + method_return_type = "void" + else: + is_method_type_enum: bool = any(method.type.name == enum.name for enum in self.enumerations) + is_method_type_class: bool = any(method.type.name == c.name for c in self.classes) + + if is_method_type_enum or is_method_type_class: + method_return_type = method.type.name + else: + method_return_type = self.JAVA_TYPES[method.type.name] + + parameters: List[object] = [] + + for parameter in method.parameters: + is_enum: bool = any(parameter.type.name == enum.name for enum in self.enumerations) + is_class: bool = any(parameter.type.name == c.name for c in self.classes) + + if is_enum or is_class: + param_type = parameter.type.name + else: + param_type = self.JAVA_TYPES[parameter.type.name] + + parameters.append({ + "name": parameter.name, + "type": param_type + }) + + methods.append({ + "name": method.name, + "visibility": method.visibility, + "return_type": method_return_type, + "code": method.code, + "parameters": parameters + }) + + return methods + def _prepare_relations(self, cls: Class, assocs_for_class: List[BinaryAssociation]) -> List[object]: relations: List[object] = [] @@ -211,14 +273,39 @@ def _get_specific_imports_for_class(self, cls: Class, relations: List[object]) - for attr in cls.attributes: if attr.type.name == DateType.name: imports.add("java.time.LocalDate") - elif attr.type.name in [DateTimeType.name, TimeType.name]: + elif attr.type.name == DateTimeType.name: imports.add("java.time.LocalDateTime") + elif attr.type.name == TimeType.name: + imports.add("java.time.LocalTime") elif attr.type.name == TimeDeltaType.name: imports.add("java.time.Duration") if attr.multiplicity.max != 1: imports.add("java.util.List") imports.add("java.util.ArrayList") + if attr.default_value: + imports.add("java.util.Arrays") + + for method in cls.methods: + if method.type: + if method.type.name == DateType.name: + imports.add("java.time.LocalDate") + elif method.type.name == DateTimeType.name: + imports.add("java.time.LocalDateTime") + elif method.type.name == TimeType.name: + imports.add("java.time.LocalTime") + elif method.type.name == TimeDeltaType.name: + imports.add("java.time.Duration") + + for parameter in method.parameters: + if parameter.type.name == DateType.name: + imports.add("java.time.LocalDate") + elif parameter.type.name == DateTimeType.name: + imports.add("java.time.LocalDateTime") + elif parameter.type.name == TimeType.name: + imports.add("java.time.LocalTime") + elif parameter.type.name == TimeDeltaType.name: + imports.add("java.time.Duration") for relation in relations: if relation["is_list"]: diff --git a/besser/generators/spring/templates/entity.java.j2 b/besser/generators/spring/templates/entity.java.j2 index 27716925f..4b3345c8c 100644 --- a/besser/generators/spring/templates/entity.java.j2 +++ b/besser/generators/spring/templates/entity.java.j2 @@ -80,6 +80,15 @@ public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent {%- endif %} {% endif -%} + {% for method in methods %} + {{ method.visibility }} {{ method.return_type }} {{ method.name }}( + {%- for parameter in method.parameters -%} + {{ parameter.type }} {{ parameter.name }}{% if not loop.last %}, {% endif %} + {%- endfor %}) { + {% if method.code %}{{ method.code }}{% else %}{% if method.return_type != "void" %}return null;{% endif %}{% endif %} + } + {% endfor -%} + {% for attribute in attributes %} public {{ attribute.type }} get{{ attribute.name | capitalize }}() { return this.{{ attribute.name }}; diff --git a/besser/utilities/web_modeling_editor/backend/backend.py b/besser/utilities/web_modeling_editor/backend/backend.py index e95aa7077..565ff6c9f 100644 --- a/besser/utilities/web_modeling_editor/backend/backend.py +++ b/besser/utilities/web_modeling_editor/backend/backend.py @@ -972,8 +972,8 @@ async def _generate_spring(buml_model, generator_class, config: dict, temp_dir: if not config: raise HTTPException(status_code=400, detail="Spring configuration is required") - project_dir = os.path.join("D:\\Faks\\Master\\Master rad\\BESSER-master\\besser\\utilities\\web_modeling_editor\\backend", config["project_name"]) - print(project_dir) + project_dir = os.path.join(temp_dir, config["project_name"]) + # Clean up any existing project directory if os.path.exists(project_dir): shutil.rmtree(project_dir) @@ -983,7 +983,6 @@ async def _generate_spring(buml_model, generator_class, config: dict, temp_dir: os.chdir(temp_dir) try: - print(config) generator_instance = generator_class( model=buml_model, spring_boot_version=config["spring_boot_version"], @@ -992,7 +991,7 @@ async def _generate_spring(buml_model, generator_class, config: dict, temp_dir: package_name=config["package_name"], output_dir=project_dir, ) - print("posle") + generator_instance.generate() # Wait for file system operations @@ -1021,7 +1020,7 @@ async def _generate_spring(buml_model, generator_class, config: dict, temp_dir: ) finally: os.chdir(original_cwd) - # cleanup_temp_resources(temp_dir) + cleanup_temp_resources(temp_dir) async def _generate_sql(buml_model, generator_class, config: dict, temp_dir: str): From 9b5c20445a8f3c66f5a4bd72aa28bfba5ae68068 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Wed, 18 Mar 2026 09:04:13 +0100 Subject: [PATCH 12/16] fix: use config java_version --- besser/utilities/web_modeling_editor/backend/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/besser/utilities/web_modeling_editor/backend/backend.py b/besser/utilities/web_modeling_editor/backend/backend.py index 565ff6c9f..ac10559e4 100644 --- a/besser/utilities/web_modeling_editor/backend/backend.py +++ b/besser/utilities/web_modeling_editor/backend/backend.py @@ -986,7 +986,7 @@ async def _generate_spring(buml_model, generator_class, config: dict, temp_dir: generator_instance = generator_class( model=buml_model, spring_boot_version=config["spring_boot_version"], - java_version="17", + java_version=config["java_version"], app_name=config["app_name"], package_name=config["package_name"], output_dir=project_dir, From cd60aefc216b7ce11be76f3168d6bcc89a4a9fae Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Tue, 8 Sep 2026 13:20:47 +0200 Subject: [PATCH 13/16] fix: name file uppercase, delete all example generated files --- besser/generators/spring/backend/.mvn/mvnw | 316 ------------------ .../generators/spring/backend/.mvn/mvnw.cmd | 188 ----------- .../.mvn/wrapper/maven-wrapper.properties | 3 - .../java/com/transformers/Transformers.java | 13 - .../controller/ComputerController.java | 70 ---- .../controller/UserController.java | 70 ---- .../com/transformers/entity/Computer.java | 45 --- .../java/com/transformers/entity/Hand.java | 5 - .../java/com/transformers/entity/Role.java | 5 - .../java/com/transformers/entity/Thing.java | 14 - .../java/com/transformers/entity/User.java | 97 ------ .../com/transformers/http_test/computer.http | 27 -- .../java/com/transformers/http_test/user.http | 27 -- .../repository/IComputerRepository.java | 13 - .../repository/IUserRepository.java | 21 -- .../service/impl/ComputerService.java | 44 --- .../service/impl/UserService.java | 72 ---- .../service/interfaces/IComputerService.java | 16 - .../service/interfaces/IUserService.java | 24 -- .../src/main/resources/application.properties | 11 - .../com/transformers/TransformersTests.java | 13 - .../target/classes/application.properties | 11 - .../com/transformers/Transformers.class | Bin 724 -> 0 bytes .../controller/ComputerController.class | Bin 4704 -> 0 bytes .../controller/UserController.class | Bin 4620 -> 0 bytes .../com/transformers/entity/Computer.class | Bin 1374 -> 0 bytes .../com/transformers/entity/Hand.class | Bin 1108 -> 0 bytes .../com/transformers/entity/Role.class | Bin 1108 -> 0 bytes .../com/transformers/entity/Thing.class | Bin 369 -> 0 bytes .../com/transformers/entity/User.class | Bin 2866 -> 0 bytes .../repository/IComputerRepository.class | Bin 640 -> 0 bytes .../repository/IUserRepository.class | Bin 1621 -> 0 bytes .../service/impl/ComputerService.class | Bin 1962 -> 0 bytes .../service/impl/UserService.class | Bin 3385 -> 0 bytes .../service/interfaces/IComputerService.class | Bin 805 -> 0 bytes .../service/interfaces/IUserService.class | Bin 1766 -> 0 bytes .../com/transformers/TransformersTests.class | Bin 522 -> 0 bytes .../spring/generated/entity/Computer.java | 24 -- .../spring/generated/entity/Hand.java | 5 - .../spring/generated/entity/Role.java | 5 - .../spring/generated/entity/Thing.java | 14 - .../spring/generated/entity/User.java | 63 ---- besser/generators/spring/proba.py | 142 ++++++-- .../spring/spring_backend_generator.py | 4 +- .../spring/spring_controller_generator.py | 22 +- .../spring/spring_entity_generator.py | 6 +- .../spring/spring_repository_generator.py | 13 +- .../spring/spring_service_generator.py | 18 +- .../spring/templates/controller.java.j2 | 6 +- .../spring/templates/entity.java.j2 | 4 +- .../spring/templates/entity_template.j2 | 314 ----------------- .../spring/templates/irepository.java.j2 | 2 +- .../generators/spring/templates/main.java.j2 | 4 +- .../generators/spring/templates/test.java.j2 | 2 +- 54 files changed, 160 insertions(+), 1593 deletions(-) delete mode 100644 besser/generators/spring/backend/.mvn/mvnw delete mode 100644 besser/generators/spring/backend/.mvn/mvnw.cmd delete mode 100644 besser/generators/spring/backend/.mvn/wrapper/maven-wrapper.properties delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/controller/ComputerController.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/controller/UserController.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entity/Role.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entity/Thing.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/http_test/computer.http delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/http_test/user.http delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java delete mode 100644 besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java delete mode 100644 besser/generators/spring/backend/src/main/resources/application.properties delete mode 100644 besser/generators/spring/backend/src/test/java/com/transformers/TransformersTests.java delete mode 100644 besser/generators/spring/backend/target/classes/application.properties delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/Transformers.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/controller/ComputerController.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/controller/UserController.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entity/Computer.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entity/Hand.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entity/Role.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entity/Thing.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/entity/User.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/repository/IComputerRepository.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/repository/IUserRepository.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/service/impl/ComputerService.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/service/impl/UserService.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IComputerService.class delete mode 100644 besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IUserService.class delete mode 100644 besser/generators/spring/backend/target/test-classes/com/transformers/TransformersTests.class delete mode 100644 besser/generators/spring/generated/entity/Computer.java delete mode 100644 besser/generators/spring/generated/entity/Hand.java delete mode 100644 besser/generators/spring/generated/entity/Role.java delete mode 100644 besser/generators/spring/generated/entity/Thing.java delete mode 100644 besser/generators/spring/generated/entity/User.java delete mode 100644 besser/generators/spring/templates/entity_template.j2 diff --git a/besser/generators/spring/backend/.mvn/mvnw b/besser/generators/spring/backend/.mvn/mvnw deleted file mode 100644 index 3933ce40b..000000000 --- a/besser/generators/spring/backend/.mvn/mvnw +++ /dev/null @@ -1,316 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`\\unset -f command; \\command -v java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" \ No newline at end of file diff --git a/besser/generators/spring/backend/.mvn/mvnw.cmd b/besser/generators/spring/backend/.mvn/mvnw.cmd deleted file mode 100644 index 8e8900d34..000000000 --- a/besser/generators/spring/backend/.mvn/mvnw.cmd +++ /dev/null @@ -1,188 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% \ No newline at end of file diff --git a/besser/generators/spring/backend/.mvn/wrapper/maven-wrapper.properties b/besser/generators/spring/backend/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index da4c7fc43..000000000 --- a/besser/generators/spring/backend/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java b/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java deleted file mode 100644 index 4895c0f5f..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/Transformers.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.transformers; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Transformers { - - public static void main(String[] args) { - SpringApplication.run(Transformers.class, args); - } - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/controller/ComputerController.java b/besser/generators/spring/backend/src/main/java/com/transformers/controller/ComputerController.java deleted file mode 100644 index e3f1e3886..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/controller/ComputerController.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.transformers.controller; - -import com.transformers.entity.Computer; -import com.transformers.service.interfaces.IComputerService; -import java.util.List; -import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/computer") -public class ComputerController { - - @Autowired - private IComputerService _computerService; - - @GetMapping - public ResponseEntity> getAll() { - return ResponseEntity.ok(_computerService.findAll()); - } - - @GetMapping("/{id}") - public ResponseEntity getById(@PathVariable Integer id) { - return _computerService.findById(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); - } - - @PostMapping - public ResponseEntity create(@RequestBody Computer entity) { - Computer saved = _computerService.save(entity); - return ResponseEntity.ok(saved); - } - - @PutMapping("/{id}") - public ResponseEntity update(@PathVariable Integer id, @RequestBody Computer entity) { - - Optional existing = _computerService.findById(id); - - if (existing.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - entity.setId(id); - Computer updated = _computerService.save(entity); - - return ResponseEntity.ok(updated); - } - - @DeleteMapping("/{id}") - public ResponseEntity deleteById(@PathVariable Integer id) { - - Optional existing = _computerService.findById(id); - - if (existing.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - _computerService.delete(existing.get()); - - return ResponseEntity.noContent().build(); - } - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/controller/UserController.java b/besser/generators/spring/backend/src/main/java/com/transformers/controller/UserController.java deleted file mode 100644 index a8db943be..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/controller/UserController.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.transformers.controller; - -import com.transformers.entity.User; -import com.transformers.service.interfaces.IUserService; -import java.util.List; -import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/user") -public class UserController { - - @Autowired - private IUserService _userService; - - @GetMapping - public ResponseEntity> getAll() { - return ResponseEntity.ok(_userService.findAll()); - } - - @GetMapping("/{id}") - public ResponseEntity getById(@PathVariable Integer id) { - return _userService.findById(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); - } - - @PostMapping - public ResponseEntity create(@RequestBody User entity) { - User saved = _userService.save(entity); - return ResponseEntity.ok(saved); - } - - @PutMapping("/{id}") - public ResponseEntity update(@PathVariable Integer id, @RequestBody User entity) { - - Optional existing = _userService.findById(id); - - if (existing.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - entity.setId(id); - User updated = _userService.save(entity); - - return ResponseEntity.ok(updated); - } - - @DeleteMapping("/{id}") - public ResponseEntity deleteById(@PathVariable Integer id) { - - Optional existing = _userService.findById(id); - - if (existing.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - _userService.delete(existing.get()); - - return ResponseEntity.noContent().build(); - } - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java deleted file mode 100644 index 97ce5a003..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Computer.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.transformers.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -@Entity -@Table(name = "computers") -public class Computer extends Thing { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) - public Integer id; - - @Column(name = "model", nullable = false) - public String model; - - public Computer() { } - - public Computer(Integer id, String model) { - this.id = id; - this.model = model; - } - - public Integer getId() { - return this.id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getModel() { - return this.model; - } - - public void setModel(String model) { - this.model = model; - } - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java deleted file mode 100644 index bc33a4f91..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Hand.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.transformers.entity; - -public enum Hand { - LEFT, RIGHT -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Role.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Role.java deleted file mode 100644 index 0811ecb7b..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Role.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.transformers.entity; - -public enum Role { - ADMIN, USER -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Thing.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/Thing.java deleted file mode 100644 index fddf92713..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entity/Thing.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.transformers.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.MappedSuperclass; -import jakarta.persistence.Table; - -@MappedSuperclass -public abstract class Thing { - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java b/besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java deleted file mode 100644 index 23aa4cef4..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/entity/User.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.transformers.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import java.time.Duration; -import java.time.LocalDateTime; - -@Entity -@Table(name = "users") -public class User { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) - public Integer id; - - @Enumerated(EnumType.STRING) - private Role role; - - @Column(name = "age", nullable = false) - public Integer age = 2; - - @Column(name = "birthday", nullable = false) - protected Duration birthday; - - @Column(name = "birthday2", nullable = false) - protected LocalDateTime birthday2; - - @Column(name = "email") - public String email; - - public User() { } - - public User(Integer id, Role role, Integer age, Duration birthday, LocalDateTime birthday2, String email) { - this.id = id; - this.role = role; - this.age = age; - this.birthday = birthday; - this.birthday2 = birthday2; - this.email = email; - } - - public Integer getId() { - return this.id; - } - - public void setId(Integer id) { - this.id = id; - } - - public Role getRole() { - return this.role; - } - - public void setRole(Role role) { - this.role = role; - } - - public Integer getAge() { - return this.age; - } - - public void setAge(Integer age) { - this.age = age; - } - - public Duration getBirthday() { - return this.birthday; - } - - public void setBirthday(Duration birthday) { - this.birthday = birthday; - } - - public LocalDateTime getBirthday2() { - return this.birthday2; - } - - public void setBirthday2(LocalDateTime birthday2) { - this.birthday2 = birthday2; - } - - public String getEmail() { - return this.email; - } - - public void setEmail(String email) { - this.email = email; - } - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/http_test/computer.http b/besser/generators/spring/backend/src/main/java/com/transformers/http_test/computer.http deleted file mode 100644 index 14feebbec..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/http_test/computer.http +++ /dev/null @@ -1,27 +0,0 @@ -### GET ALL -GET http://localhost:8080/api/computer -Accept: application/json - -### GET BY ID -GET http://localhost:8080/api/computer/ -Accept: application/json - -### CREATE -POST http://localhost:8080/api/computer -Content-Type: application/json - -{ - -} - -### UPDATE -PUT http://localhost:8080/api/computer/ -Content-Type: application/json - -{ - -} - -### DELETE -DELETE http://localhost:8080/api/computer/ -Accept: application/json \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/http_test/user.http b/besser/generators/spring/backend/src/main/java/com/transformers/http_test/user.http deleted file mode 100644 index 8bd7f242f..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/http_test/user.http +++ /dev/null @@ -1,27 +0,0 @@ -### GET ALL -GET http://localhost:8080/api/user -Accept: application/json - -### GET BY ID -GET http://localhost:8080/api/user/ -Accept: application/json - -### CREATE -POST http://localhost:8080/api/user -Content-Type: application/json - -{ - -} - -### UPDATE -PUT http://localhost:8080/api/user/ -Content-Type: application/json - -{ - -} - -### DELETE -DELETE http://localhost:8080/api/user/ -Accept: application/json \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java deleted file mode 100644 index c876adc49..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IComputerRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.transformers.repository; - -import com.transformers.entity.Computer; -import java.util.ArrayList; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface IComputerRepository extends JpaRepository { - - ArrayList findAllByModel(String model); - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java b/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java deleted file mode 100644 index 9cf960796..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/repository/IUserRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.transformers.repository; - -import com.transformers.entity.Role; -import com.transformers.entity.User; -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.ArrayList; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface IUserRepository extends JpaRepository { - - ArrayList findAllByAge(Integer age); - ArrayList findAllByBirthday(Duration birthday); - ArrayList findAllByBirthday2(LocalDateTime birthday2); - ArrayList findAllByBirthday2Between(LocalDateTime start, LocalDateTime end); - ArrayList findAllByEmail(String email); - ArrayList findAllByRole(Role role); - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java deleted file mode 100644 index 1f700948d..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/ComputerService.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.transformers.service.impl; - -import com.transformers.entity.Computer; -import com.transformers.repository.IComputerRepository; -import com.transformers.service.interfaces.IComputerService; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class ComputerService implements IComputerService { - - @Autowired - private IComputerRepository _computerRepository; - - @Override - public void delete(Computer computer) { - _computerRepository.delete(computer); - } - - @Override - public List findAll() { - return _computerRepository.findAll(); - } - - @Override - public ArrayList findAllByModel(String model) { - return _computerRepository.findAllByModel(model); - } - - @Override - public Optional findById(Integer id) { - return _computerRepository.findById(id); - } - - @Override - public Computer save(Computer computer) { - return _computerRepository.save(computer); - } - - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java deleted file mode 100644 index 3cda081d2..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/service/impl/UserService.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.transformers.service.impl; - -import com.transformers.entity.Role; -import com.transformers.entity.User; -import com.transformers.repository.IUserRepository; -import com.transformers.service.interfaces.IUserService; -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class UserService implements IUserService { - - @Autowired - private IUserRepository _userRepository; - - @Override - public void delete(User user) { - _userRepository.delete(user); - } - - @Override - public List findAll() { - return _userRepository.findAll(); - } - - @Override - public ArrayList findAllByAge(Integer age) { - return _userRepository.findAllByAge(age); - } - - @Override - public ArrayList findAllByBirthday(Duration birthday) { - return _userRepository.findAllByBirthday(birthday); - } - - @Override - public ArrayList findAllByBirthday2(LocalDateTime birthday2) { - return _userRepository.findAllByBirthday2(birthday2); - } - - @Override - public ArrayList findAllByBirthday2Between(LocalDateTime start, LocalDateTime end) { - return _userRepository.findAllByBirthday2Between(start, end); - } - - @Override - public ArrayList findAllByEmail(String email) { - return _userRepository.findAllByEmail(email); - } - - @Override - public ArrayList findAllByRole(Role role) { - return _userRepository.findAllByRole(role); - } - - @Override - public Optional findById(Integer id) { - return _userRepository.findById(id); - } - - @Override - public User save(User user) { - return _userRepository.save(user); - } - - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java deleted file mode 100644 index 65d57fa49..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IComputerService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.transformers.service.interfaces; - -import com.transformers.entity.Computer; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -public interface IComputerService { - - void delete(Computer computer); - List findAll(); - ArrayList findAllByModel(String model); - Optional findById(Integer id); - Computer save(Computer computer); - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java b/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java deleted file mode 100644 index 1a2f5de89..000000000 --- a/besser/generators/spring/backend/src/main/java/com/transformers/service/interfaces/IUserService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.transformers.service.interfaces; - -import com.transformers.entity.Role; -import com.transformers.entity.User; -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -public interface IUserService { - - void delete(User user); - List findAll(); - ArrayList findAllByAge(Integer age); - ArrayList findAllByBirthday(Duration birthday); - ArrayList findAllByBirthday2(LocalDateTime birthday2); - ArrayList findAllByBirthday2Between(LocalDateTime start, LocalDateTime end); - ArrayList findAllByEmail(String email); - ArrayList findAllByRole(Role role); - Optional findById(Integer id); - User save(User user); - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/src/main/resources/application.properties b/besser/generators/spring/backend/src/main/resources/application.properties deleted file mode 100644 index 62baa2e8c..000000000 --- a/besser/generators/spring/backend/src/main/resources/application.properties +++ /dev/null @@ -1,11 +0,0 @@ -spring.application.name=Transformers - -spring.datasource.url=jdbc:h2:mem:testdb -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password= - -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=update - -spring.h2.console.enabled=true \ No newline at end of file diff --git a/besser/generators/spring/backend/src/test/java/com/transformers/TransformersTests.java b/besser/generators/spring/backend/src/test/java/com/transformers/TransformersTests.java deleted file mode 100644 index c0538266b..000000000 --- a/besser/generators/spring/backend/src/test/java/com/transformers/TransformersTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.transformers; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class TransformersTests { - - @Test - void contextLoads() { - } - -} \ No newline at end of file diff --git a/besser/generators/spring/backend/target/classes/application.properties b/besser/generators/spring/backend/target/classes/application.properties deleted file mode 100644 index 62baa2e8c..000000000 --- a/besser/generators/spring/backend/target/classes/application.properties +++ /dev/null @@ -1,11 +0,0 @@ -spring.application.name=Transformers - -spring.datasource.url=jdbc:h2:mem:testdb -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password= - -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=update - -spring.h2.console.enabled=true \ No newline at end of file diff --git a/besser/generators/spring/backend/target/classes/com/transformers/Transformers.class b/besser/generators/spring/backend/target/classes/com/transformers/Transformers.class deleted file mode 100644 index ec08e47ebf1eb5cbce3ae25c283b83baad193510..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 724 zcmaKqO;6)65QblumXuPy%JQ{<5Vz6;zH#X;5>(<8KD0&M!=9Yr218<3j>GQX+7oCG z`~ZFw+HpW4hl)s<(ad;W+mGkv_1`mq@7QgjhM55MHX4{^SiTbvf~P_!{4%QGh1HZfr8{tkC(`*i71-Usr+_T9^;eZesz93_W8Lo=>dO$=Hfa{xEyzaQ@Z%VRROEU8S0yuaUaWr*PE(GJcJhzo1Ea?(DXfT6 zS=oZpzT9W156q2ZSPGSv7ey9Hd+paUbV3u0bR?|u@q4oFZdJ~(7k+wmFPjNPS6J^( zJMoC>!X_lNI;BkX&!xLHH&?=gas-y4H8h2d<(cwFTd8tB^o$s`eiYiNOpa8p{9Uy+ zuB1uL$5RoN@I~Pau__tK_oq0eSE;b-V;=#HhyQ9|en7}BQN+nJ@{Qgj*x$0eOxBk{ zEh}W(Zwt`DDn)bnf;AHH%}1pe(Ch@gC(Ql)==ZY-nw|jj6^$m=u|fGw`Wr-+Tn$^L M{fce!e%l@3KPSbyvH$=8 diff --git a/besser/generators/spring/backend/target/classes/com/transformers/controller/ComputerController.class b/besser/generators/spring/backend/target/classes/com/transformers/controller/ComputerController.class deleted file mode 100644 index ad34ac7516b7c75d6b670ece715000950f34db63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4704 zcmcIn{Z|~-8Gh~t*Z@PaB{3SKkyuP1U?wKy!$2^A!BCfkLc~_BW_I@i6J}>N^Fc&w zwe?$DKic~BtABx><4Hq)=<(;C{z*MOeeTTc?7&WvJ*jdId*|Le@7MD_?|bk3>%X`D z3E(Wg=|T?M@@Ui0jt+ssRb$Q2EyG^cFO{y!vMLy#^}j-iaCJetYi3yroGdme@uEH5 zv?;!1l%=Q7)V0NOqutnnop}^A`~tlKBln_2AireVl?lrd=p7g=hQSBEY3W7N^GCa| z8~5e0N5lPiK;V$$F6&;+HSOgk*Qmci0OhsTYt#pR)EK8SK8;)P9`KDtV)@U#DyI;d2=o4r+ zYBh^~J+djrrNAz$m-XrRfdM~?U*^%T;V~Q}B+Y8cf~rvyI3D{Eg4L|(HXaEKXc)v{ zYFU<~)@}S*2`hGuFtv z>49eA$uNNlbBSydt+u|Rd>qkm0w>8P(>q_SDF(1sUQqNot>HpLPP9DLlg@ zw8(<~3(}~tswM-|;z&}A;cOn`8lJ*AR?fYtAs<)Z1CB}-1&OpBpvz!Nx~nU#`cbJN*_e8*kajYL472z=+d>B`C|88zip z=-bYsY0Ha2wItok2Gz5IicZcgH;nYtIh>;**5&Z zr46I?B#xyTh3ar)%`R-E_xR{I4Uc4Eb6uA75?fOm7S`u1yA69J#q#tw>RaQP9LUs~ z(NU=?XOc;~QXRG4{CK9!N`J+v%o(aF^S~gsrT@!ZOR$jNYlamt0qy$hX5|fmM>mrz z@n;juPG-iOh2)&!uhf~Na$k0(p(>!G=yZ3qTJH0-wOzi-f@ImS8K$ZvQNJNha%CTF*`i?qH2m$+jhe|@eROGOvz>Gt-{Y0P7Ij^(fVTw>rXh|# z9O}2y^ohQ+1G@@XE?|aFA9V3s;zJFu<6HsfxbWvL`~{!n@u`N-@HzWYc>N6JO?%B* zm3lG6j)sl+Ko;m`%Wus7o%SOZiu}A_*cD59{YA%F4QivQgJi*1H~RF-Cc(=ruY22! zOx{^GeN~OkP8pW9VEU4I(`Ia&-%pm|d6K8vj?LG>Jig$4s0pO3c=Py5VE>(8WxtES z!2u>F$Il0zy6Q8JHCS}{XcC3NV#m8^p_&3mQ;?xGn zz3+yHZeT*~4{w0Jo@v|L7JJ(kruac785pKFN6Px z7>DB4A=Ge&%5&Xhgl1Wa2tS)0ybW zaUse?gu$x`N4_Nn3g3|i2D8IkFbKoHfoFci=6{2M0K+zs@A9 zoQPC~_A!j#b1jEALe0H=`UB^0CfeV^A30Y=@iF206W+nQ;nRC~A0Oeb+*2}Rto6Tj NbmH&$5?^EQ{{U^(H@N@+ diff --git a/besser/generators/spring/backend/target/classes/com/transformers/controller/UserController.class b/besser/generators/spring/backend/target/classes/com/transformers/controller/UserController.class deleted file mode 100644 index d59d4d5c18872ac0b59f45a6db293d494b485107..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4620 zcmcIn=~Em<9DTh3HVZ?tk{FHAgeWEuFcS$$G!P764s=NnBHqXBZUYl$XVy80c;EMZ z=c|8#R#`FRgXL$-KgqIuJu~diW;bNLsA7A%d-`|0-|N>szx{dT7XZg_u^lOFNTWqV zE7}CI=Z$4Uw+wqhKbt=<3%)?xh-sVtsKAEazPSuGB9lhDh7M=~`wC7;_g%yG<{h^r zT~9AKw(mNYC0+etPr4J8u7LJTz>!($E}I1@Fqo@b%xN{Jo3=0Ad7~gbeNtJ8*9>=H z6E>%j)o=s41P=UN0|M!J(=Lu#mOxi;UoK2G@J&n4nVvt~j;**cjcpok!p#DE9d|+Z z%C2cI%)3TOUU1wcebM*J`i%6-j_t`4wr~0?0x4%n;8<^DF9F_gU#>|TVQRN%*nwLG zHmV@US0;-BeMzH}gk?dxv|0`PY}q#*+pvbaSkIjrcA;CK)hL%O#4C%nW&uR|W<8n-7IG7{^2!_iH$TlLEJ{2MRrS(pNm)){v)j3bfOT z0+Rw;*T{HvogUPX!xV+4C@tx;QoFBLDcaqNvl^xq8_nblV z+%Y+)U|4g8YpQ-c*y1mm^fyq?r)!EMeYA0hgu;}WH5Y8d4_w-Rs`q0_4FB?`s;9K<<&Em@fx7QM?5=#xU4mXYBwBawVu|?Iq z;7UU=e)4*ZZ|I@HS)`0cWfs`EJ_FR|r7#kH^S|=m1i*(HX3~gp(~g&*T=VN@xG|jyG z!*i%AcUj9Q<%>p7ta?2M1lnhvz%9s=raA?-)ZTRWD@DB4dDDpLyiWSni!#a7_^4{U zG$ea9){JhHOTu+3CpTtviqoSIj1(m^0Ww?J8+f%rL+ki{heFb|} z+y5)JUhSzn^t3Hn8GE7)JdI}<=Cicd%IgA~$h{G|-^8gW_@C#GhXRV6Pph;f9|Y#H zkcepm$FgxudJ0nmHTkOVX7IDA1AD?qx(O5CuSAl?B23!spvCiiO0C7T$k(EnmMV}~ z9IM1qVn@J+6UTCyYg;)sSjFeeU6*k1BDBjmvI=tVhk^b}7*q2Dt6;20Tei2v(YAyo zzJ)~w1{lqLbdd);Fo`3$G|15i%R9J;-*2|SjT;S4VB|1|ah$<0b@O*_M-xC9c?@$$kse!97 z2-ClWQ@>(-6%V9H&odD?To<9REzEM5@g5@zqeNj0{g_D9Z>WO6P>etaKVDScsYJGF z#OFwTwl$nvsbu=XRhh1FF59jYm6K{8>}IR}GL@>bBN98bn`yknu@qhoEqC$h74~1P rSbq(#v#&PAhlJ}5yoI;Jr+4r!KEM~8Q)@=J*S~Db;46HC@38$Z@<9{z diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entity/Computer.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/Computer.class deleted file mode 100644 index b0a168834d9f6a50271f352a3afc4773d820e389..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1374 zcmai!>rT`_6vxkWw|l|GMQ(yzL`2+OkUjuPVi3(HK#XiQ!EeLvxK!IIX{RQzZ@HA86%5IRVp{gri!4YLEt+klsftpguV(R zM``1lbEkLe>XD$zOIN#QM^LHN?$@bC^(u`u=pHo$t=#6j8~A6jQK2Bq9SWM-|17^s z$CG;GbhS|<6>f8=`weQ+gdp1whRPE(nX@}GA@Xkvn*R_3$X9(ga!)+ISq#IWe7%03MuT7GF?n-rq#~*T!w)@ zO7*ivSvY150S?khPTz=eHOm@8WEL~|C^@jPqaY3k>aEKi8NXblEncA88ork-Eb3n< zoJZ0~XzS=;vXv1%!SY!chJ|rplOZ4A_LxdEiE|3~X+<+MOAlc(7r|!!BR#@1)-^Nc zGt)LxZ3Qp*NkJ|kh=Wt$v^zgZe8-1Hi;%1)1YUyVyfC0;Wah=NX$4m7e2kN~rOD2n zjSH%FHZN%WI?N=Mp`L@Br#i^)!;M3(q;6SA1jx$cFJ`w;I#J!iGe2?GReB0}6Zr>K zzd@NtUM@slqh}fN4o+@qCBeNW<-Z$35*ulla5lBNBCKSD%w_X5m}#A!7ch9QHsFTQzWK){BZ{q#}lvt0ge*|m~O8@`> diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entity/Hand.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/Hand.class deleted file mode 100644 index bc00d4d8c71418737ac657f0c9d4c47847355a9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1108 zcmah{VQAai@3edR^1&6->vs z{P%@@?z9y!gf&DErD#<5>I@6UBtls*Vj2{|h`c#?v0oRJTN*UP8RTr^x$(AIV_1AQ zSw}?zNf~hs(@2q2%XS@;A)V8WQ+~z^Hg`IO!{({k^2;jHxFcgm!z}JHO#J~=oqm_P zo^iWx9v(An=Em5&Ht+SyV}+XUSx%=s-a!Zi`WX%PFwYRJSdQgyGt7)jjhF{|BZi1d z17~7TL|oFaj1@6>aEBgMa{3LUQsr)27P$Kf3FaH_DJQ z+!nVR+_QwpNGa^_E(xZt?|~ut()3T<_G|7D#wTx(3;Yv{f|@mHGua_GK1^J%Z8_X# zP-<4k;eOvET`8wG$Pty6J=$AwtdZDmW%8}J%hasq_Pv(5XNk!b;mbAgiFZo0;51oT z+L;p;1t^m7fMiD4jZPwzXR$)Khm>+ zQv^y%2v3v18a;)8QcSACQq)q6q$5mkU^?;*>_{3wsvU&~kZac`i=l+#h_*GNEqx_c q>b`V=g>y(jvKS5$`4SJ#p^9q`7o^g4L9`4_5gOfP9nYv1!p1KXzUGJk diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entity/Role.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/Role.class deleted file mode 100644 index e4cb7160b4163acf1dbe5d792306e35685219e85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1108 zcmah{?@!ZE6g_Y4)~$AI%n33l2uxjvvJK(W27%yANCp@xONgJ|wlYe(Ht9MNzxgNW zNQ7uKeD;qr-q+1IgU%-H?Y-}ubAP=1{m18T05-9qAjFVux!t1gai@3cdR^1&6-~#t z{CCA8*EVBdkTrx6VF>TM+}}H3n6FP_R0JieAtosDu~9n`fjb&B#2J)aW2b&xGZ+@$ zPL@%TKvF?m!!%Nqyk)zN$&k+L^>co~i#B&U#lz;g+43tY(zvH!M#C)bGfe#k)SP~o zvR-hzZyug9Y~;t-yEgCjDq{h|_bjJV8P6aD0{x7J2gou+s+MER&2;G-FN4ip8Z_q+iTlQ#frLja}y4A^5Z&fIn;r6|j`N|TV$HZQ)iYwkO(}2^o z%F_0nSW$p1?H!QJh;^frF!?zw6Yep2OJv2P4xoO*)D03J$YXdy-$-yK5q=g=5eo3@ zWCcS_vBEX(-hc^w=ns6JBH9oI9R>1M=nXUI?*){022vnaCM(gAQdr4eVeTXS{!2;0 zGtot$6bs>b5?H0T7*I+{Rjd@H6fNnH=?hGUzk;1e14zb+Jb+@{?ktKjN+a6lh_?8J rSgD8773MD?1 z2^^NTmcfzn&(_t-d8tg$;Z4p~+AIZI!|A|Rh zX)kb;|LZyt$fmY&rMlD6Dr;w5#JBz9;#bUdRQK8&0Zy zP^MJ!o;Dk`m^Q4H6?s2Nj?w2;#3T6fG8ng+$(94@`3J;13ms&P@fjFkr^VQ(%yzkU Ou-D=cBd+n^KJW{VHdv(q diff --git a/besser/generators/spring/backend/target/classes/com/transformers/entity/User.class b/besser/generators/spring/backend/target/classes/com/transformers/entity/User.class deleted file mode 100644 index a02bda9088c3304ba319db17097aa563341e8e6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2866 zcmaJ?+fo}x5IqCB0ZX{c77z>$cEs99iE}qLHh0Ux;DD@D{E*aw#;}gGa;;Xb;s^3Q zNmW#F7$cN@o6ZPW5}nXbOaCk7UAMqeE|#ivTZ}#0(T*l%U9HF~&@9uWwtN}} zcC814ET6seJxZHfd*xF3C%#SW9UF~WtL_jx;aGLboN-#UnnmjU%qb|$$9io!ExxW$ zqi51j5_zGx_{20MlR7LPl=gOb*G8ktyGC|xNAI=jN7^mRm7=N0Ua?$T_Tk5bcWP6< z-U%SdMIOK9N#H!=!xUb*K5OC}e2mV73C#R&op}-Q4^v>N!oqu@z0*eZrR7pmk8e$l zPPA95Doo|$s)*;!;5-`_gJ7l!Ac@WwA;h(%Kx3i>#tZrDZ3Q|-=rt+K6y5yA5WlQn z)KK=EbEhZ@L$tp%yAe|Fw2}*q2pr37rUS22P#Q--X*?T6?X(r>457DzdYn9m^*i20 zMP`T>T1-X7jl9?3VWZ_9>up=)9gu7)ND}lanJvGiG2v$T{8tOw&sr&q7J-^rU@Plm zDWzCUY;PRza(4^;_>^;=`y~w%_i&$P%T3x8UZco0n_p-=%=t6^KG%4FFSuZ@4Ua}S zhmK=`V?ui1)zjJYQ+B$qz`k zG5iNV^x-Rx<4AD4%W;}6GcdURnqQ2-3E$vb?ga2VdI;bL&S}ntD4=<-jS2I98(DLi z_+~EA# zzjG0&C3yk9>!m`;3xytIHB?AuKZ|@-E+dz}e9%{X1R)z(T|_3-Bv*uIQW8)&J;76s z-JG)BoF2wZy_o6|uo zr|+>F$7!ycQ-z#)pwe_od(tltP%*oTLPz9l~MQOo%+MTYcyUCKwDtq&HdGH7Lqr_ELTY?c>HnWa=(cu?go1|T2H7f@nl@^ z06Y|8oP!+^teT38)!P0&GvAJO{-3|T{{VnD@T?342X=cV5M-qe`^E-nLt$}fLPf?# zqIn#mZ71(#SaM)}AiqlCOYMon?f`pKhLQt&#(E+gTBW_dl>wd_`$f2tq!opw$GZ zZG4`Hz3t449{-TZwq(cQ$L9&pf%$E2@+ZbpYn{tf&Zsq4W@a$)XkYA@k?rAo6}M>X z5950go00?7_J|E3z|SgFT_5XO8yb&0JJ0T4VMDehG!zX{q^?(EzvP?AtH?%NVoxo@ zG8CY~Z$&2I?Gmp7tgy5SYpHZOk=9w-fX!68l1N)DU4?5?sZxOJEZs=tD$6(FRvLLb ik?yc`7w)Cf{X}}eyc4L0@QA+_;4wVmy~L+@3h*1_yz_qm diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/impl/ComputerService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/impl/ComputerService.class deleted file mode 100644 index 7e1040f92c2af8247f7b6d197308178751e48872..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1962 zcmb7^TTc@~6vzLwTneQW3>^luUf(P+WTJp(KP~{at6F-l?KILDO<@e<46VL> zOc$HHJ~qp$x^yR)I z)?C-qw#nOPSS)y|%>Ak<&4IEj;>1&*`Mw}QapDAAnXa1McI@^vulrx>*GOdl<^z7zxG9-7snktG7X>>J^ zzc|fi&D@|jdZmPYYN_yay()O{5OVZ7J80OFr08dv`APN}P2qO3nZVKzrd5_!oGc^T zHuW9sEB&!B6*vLin5O>*Kn{408M2IK;VgNX;5=E2Y}eE`B&JW%ZQ&eYMm{irB+WO2 z8EU!)7z(1ZUl4ANzO~VtAwzSr;bz$Lv@+)=4f_=R3}0v!&2Bi7T{6JFZ1}vQNSc*s zX=J%dQPC{#S~1k4Ycib2YeEGX&J(-IU!ctqyt#(|AkLq-hQEln=lsQx|Bg1tkfW?k lCDw!d(?OLVTlj^E%hkw4j!1_^PREOEVX0B13S}tJn=gI;Q0w*iM5xuHka$SX8(n z8b@_wmu6*ufnoG@lcwAJ$aaOaofgC0Qf#{uzBX3X?v8k27$X=>V@$_oj5Fl_$6h)_ zmDjoC4A-)y^G7V?_7wZ7j%&EikUkJrwOFq+jAwJca8ru9Q4+57jJT;|0`D>Cf#6cB zSR?XGl)P$5UgJ(73Me{`*$OF>IzGTHhRY#kNjUPbYLa8}0ZNJnH&&XCDTQryVcpg- zg^$SK80!JUY!p_qD`tJglzfNKU5KCP_!QF&6LI1amnWQC46Dgt-))}xy=u(r$YF+I zB-Hq{VG4>uF6!5^bc9vwV)z{$cX5y5Qi$2M>lB^2Y&_YxC53DmN}vmIR>ysi9|sk2 zskK^V$VN@_>*Nez^GF5Etb6J5P{$m;VCZwrV=`nqd2gib3TZrIn2hCkV6rNWG#*oM zV-obnFF?vb=fJFR*RP~a{4-m+b#ub*SW&^%@kzZgTo3x3C`cpJ}icrDoX97&JjqXg7ZE@ZC1_pnM_uP<~B! zFFS@z*={-&{!FM|Gp6>G{Z4mJednoHU)a)IDtT_p))6-X@dAB92kA1z@D=?ub-$sp zK)0j;8s}*orjag5^n5}ygGNSU|ID9YztbNL3-na4fiW!7QvpZ-U!zDPed@79Qfl)u zjT()^Gk+kRKgA^t9fZ1)0XLArO%HS37Y#7Vj%BP6?kP$2GnyziU0p~_u^-Y*>CGti zDXuX5Mzg4OlM(AO0rr_E^9}8!yA4tvX_0nCt-PVdP*VvOKF2Dd0t=rL+vl&*BHelt z`0vO0(+T`_lsf$NkpF@f=_ZuG|1i$aC-670+2L=5{4H9fdr`n&qcKJAbT5i`PVsRn z=AcYV{24o~&QyP9In12P{y+j ze}F`G*K@vpKkYdmpKtE~a1WOjlnC6oQAmwqQqLk4a;2%}YAIY!g*05vn9DUCJx1Xo z#?r|#3(5pqbGBqOVA7}4mpOM0fvU#?Za9If?s!X=OCwA|zoqQnGipq@d5yd$rdWtX zr3uuKn0XPfWl#sst*SxP3OI8UgVa}NpvYOh0tEJONdYRjXceepZKV~i w2c)}KhX%Ixu~mWttPi1yr`44sZ0Ar5p<`(OLMK1aDMB4MTNB3OJd?cm0@iKlsQ>@~ diff --git a/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IUserService.class b/besser/generators/spring/backend/target/classes/com/transformers/service/interfaces/IUserService.class deleted file mode 100644 index 065e15ebfab21a67055af2d44f62e72c3bc16ce3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1766 zcmb`H+iuf95Qb;doM}rFj^*5PN?NEst?V^guY>$*T<`!q z6k>enI#F;ih&%6o`_KF{-kl#mzkLUQx3E)#5`kBy=Nd{<;g3D(GU*#WlM`+-gA2vv zn3~Kt2B)|=O7?3|Ca`uvC)9ALu#Ka$3uY<;Rf{=HF#;Q%;ap566jz}Uh1u(VM*SgE z=bm*;C3R6GeFEhGA%WVM3#;ci1lpbMFisOF?ifSvt33jZ5w`_Zfy6|+a|hlm&b*HS zmectCuxDe8txl37E@glf+mXYmygeytm?mGMk>o>RNdY@8ZKuzrI=5&@U^`8qxXX-# zKvKm$k!z}criCAi3kz6l&sp2W>oT2U=$X_xpo*QK(_Dj%OM@ss{!4iQo9pv7`%Hah zOb{64r%Qfu+~Q(oRD4Ax-t#3UECT$0MJ`a`s;=;zOF6!bZkjy4v?C?Cuyea#VTtCl z6@SzeFtnl#eexW95wG)Kd~_h!TUo~P(|?f{uvw36_QL`Gu9=Sg?wKEbi5?ksa6*(g z3TE21NKn8>*{2ii(tduK$y{7Kya+T$ULZ~Oo<}`zP0!?=XiioM;O)fG4$#CAU4bgD zYq+Wqs6#Zh8_>k9E4WpHWn8boDt@I$uHtrr))2LzouO+QT}N~SZf59~Mz;~I!<`g0 yN^lp^J&o@pegF?MGzx5eIGc(%BSC<$uAl3Fmsh!GYUlochue3H!IAt`|b`oPpd|c8Bgj40D z;~`yqnCOO diff --git a/besser/generators/spring/generated/entity/Computer.java b/besser/generators/spring/generated/entity/Computer.java deleted file mode 100644 index 59e80d640..000000000 --- a/besser/generators/spring/generated/entity/Computer.java +++ /dev/null @@ -1,24 +0,0 @@ -package spring.entities; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; - -@Entity -@Table(name = "computers") -public class Computer extends Thing { - - @ManyToMany - @JoinTable(name = "many_to_many", - joinColumns = @JoinColumn(name = "computer_id"), - inverseJoinColumns = @JoinColumn(name = "user_id")) - private List owners = new ArrayList<>(); - - public Computer() { } - - - -} \ No newline at end of file diff --git a/besser/generators/spring/generated/entity/Hand.java b/besser/generators/spring/generated/entity/Hand.java deleted file mode 100644 index 228c9d9d0..000000000 --- a/besser/generators/spring/generated/entity/Hand.java +++ /dev/null @@ -1,5 +0,0 @@ -package spring.entities; - -public enum Hand { - RIGHT, LEFT -} \ No newline at end of file diff --git a/besser/generators/spring/generated/entity/Role.java b/besser/generators/spring/generated/entity/Role.java deleted file mode 100644 index df68f847d..000000000 --- a/besser/generators/spring/generated/entity/Role.java +++ /dev/null @@ -1,5 +0,0 @@ -package spring.entities; - -public enum Role { - USER, ADMIN -} \ No newline at end of file diff --git a/besser/generators/spring/generated/entity/Thing.java b/besser/generators/spring/generated/entity/Thing.java deleted file mode 100644 index facdefe06..000000000 --- a/besser/generators/spring/generated/entity/Thing.java +++ /dev/null @@ -1,14 +0,0 @@ -package spring.entities; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.MappedSuperclass; -import javax.persistence.Table; - -@MappedSuperclass -public abstract class Thing { - -} \ No newline at end of file diff --git a/besser/generators/spring/generated/entity/User.java b/besser/generators/spring/generated/entity/User.java deleted file mode 100644 index b65a6107f..000000000 --- a/besser/generators/spring/generated/entity/User.java +++ /dev/null @@ -1,63 +0,0 @@ -package spring.entities; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; - -@Entity -@Table(name = "users") -public class User { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "email") - public String email; - - @Enumerated(EnumType.STRING) - private Role role; - - @Column(name = "first_name") - public String firstName = "John"; - - @ManyToMany(mappedBy = "owners") - - private List computers = new ArrayList<>(); - - public User() { } - - public User(String email, Role role, String firstName) { - this.email = email; - this.role = role; - this.firstName = firstName; - } - - public String getEmail() { - return this.email; - } - - public void setEmail(String email) { - this.email = email; - } - - public Role getRole() { - return this.role; - } - - public void setRole(Role role) { - this.role = role; - } - - public String getFirstname() { - return this.firstName; - } - - public void setFirstname(String firstName) { - this.firstName = firstName; - } - -} \ No newline at end of file diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/proba.py index 15e97bb49..2d2747916 100644 --- a/besser/generators/spring/proba.py +++ b/besser/generators/spring/proba.py @@ -8,7 +8,7 @@ from pathlib import Path from besser.BUML.metamodel.structural.structural import ( - BinaryAssociation, DateTimeType, DateType, DomainModel, Class, FloatType, Generalization, IntegerType, Method, Multiplicity, Parameter, Property, StringType, TimeDeltaType, TimeType, Type, Association, Enumeration, EnumerationLiteral + BinaryAssociation, BooleanType, DateTimeType, DateType, DomainModel, Class, FloatType, Generalization, IntegerType, Method, MethodImplementationType, Multiplicity, Parameter, Property, StringType, TimeDeltaType, TimeType, Type, Association, Enumeration, EnumerationLiteral ) from besser.generators.spring.spring_entity_generator import SpringEntityGenerator from besser.generators.spring.spring_backend_generator import SpringBackendGenerator @@ -91,39 +91,117 @@ def proba_jednostavna_klasa(): # package_name="spring.entities" # ) - # generator = SpringBackendGenerator( - # model, "3.5.11", java_version="17", app_name="Transformers", output_dir=current_dir / "backend", package_name="com.transformers" - # ) + generator = SpringBackendGenerator( + model, "3.5.11", java_version="17", app_name="Example", output_dir=current_dir / "backend2", package_name="com.example.faculty" + ) - # files = generator.generate() - - json_model = class_buml_to_json(model) - - import requests - - res = requests.post("http://localhost:9000/besser_api/generate-output", json={ - "title": "spring backendaa", - "config": { - "spring_boot_version": "3.5.11", - "java_version": "17", - "app_name": "Transformers", - "package_name": "com.transformers", - "project_name": "trans" - }, - "model": { - "elements": json_model["elements"], - "relationships": json_model["relationships"] - }, - "generator": "spring" - }) + files = generator.generate() + + # json_model = class_buml_to_json(model) + + # import requests + + # res = requests.post("http://localhost:9000/besser_api/generate-output", json={ + # "title": "spring backendaa", + # "config": { + # "spring_boot_version": "3.5.11", + # "java_version": "17", + # "app_name": "Transformers", + # "package_name": "com.transformers", + # "project_name": "trans" + # }, + # "model": { + # "elements": json_model["elements"], + # "relationships": json_model["relationships"] + # }, + # "generator": "spring" + # }) - if res.status_code == 200: - with open("project.zip", "wb") as f: - f.write(res.content) + # if res.status_code == 200: + # with open("project.zip", "wb") as f: + # f.write(res.content) + + # print("ZIP sacuvan kao project.zip") + # else: + # print(res.status_code, res.text) + +def proba(): + # Classes + Patient = Class(name="Patient") + Medicine = Class(name="Medicine") + PatientFile = Class(name="PatientFile") + Person = Class(name="Person", is_abstract=True) + Doctor = Class(name="Doctor") + + # Patient class attributes and methods + Patient_patientId: Property = Property(name="patientId", type=StringType) + Patient.attributes={Patient_patientId} + + # Medicine class attributes and methods + Medicine_id: Property = Property(name="id", type=IntegerType, is_id=True) + Medicine_name: Property = Property(name="name", type=StringType) + Medicine_m_heal: Method = Method(name="heal", parameters={}, implementation_type=MethodImplementationType.NONE) + Medicine.attributes={Medicine_id, Medicine_name} + Medicine.methods={Medicine_m_heal} + + # PatientFile class attributes and methods + PatientFile_text: Property = Property(name="text", type=StringType, default_value="This is patient file template text.") + PatientFile_id: Property = Property(name="id", type=IntegerType, is_id=True) + PatientFile.attributes={PatientFile_id, PatientFile_text} + + # Person class attributes and methods + Person_id: Property = Property(name="id", type=IntegerType, is_id=True) + Person_name: Property = Property(name="name", type=StringType) + Person_birthday: Property = Property(name="birthday", type=DateType, is_optional=True) + Person.attributes={Person_birthday, Person_id, Person_name} + + # Doctor class attributes and methods + Doctor_isDirector: Property = Property(name="isDirector", type=BooleanType) + Doctor_calling: Property = Property(name="calling", type=StringType) + Doctor.attributes={Doctor_calling, Doctor_isDirector} + + # Relationships + person_medicine: BinaryAssociation = BinaryAssociation( + name="person_medicine", + ends={ + Property(name="patient", type=Patient, multiplicity=Multiplicity(0, 9999), is_navigable=False), + Property(name="consumes", type=Medicine, multiplicity=Multiplicity(0, 9999)) + } + ) + person_person_file: BinaryAssociation = BinaryAssociation( + name="person_person_file", + ends={ + Property(name="patientfile", type=PatientFile, multiplicity=Multiplicity(1, 1), is_navigable=False), + Property(name="owner", type=Patient, multiplicity=Multiplicity(1, 1)) + } + ) + Doctor_Patient: BinaryAssociation = BinaryAssociation( + name="Doctor_Patient", + ends={ + Property(name="doctors", type=Doctor, multiplicity=Multiplicity(1, 9999)), + Property(name="patients", type=Patient, multiplicity=Multiplicity(1, 9999)) + } + ) + + # Generalizations + gen_Patient_Person = Generalization(general=Person, specific=Patient) + gen_Doctor_Person = Generalization(general=Person, specific=Doctor) + + # Domain Model + domain_model = DomainModel( + name="Class_Diagram", + types={Patient, Medicine, PatientFile, Person, Doctor}, + associations={person_medicine, person_person_file, Doctor_Patient}, + generalizations={gen_Patient_Person, gen_Doctor_Person}, + metadata=None + ) - print("ZIP sacuvan kao project.zip") - else: - print(res.status_code, res.text) + current_dir = Path(__file__).parent + generator = SpringBackendGenerator( + domain_model, "3.5.11", java_version="17", app_name="Hospital", output_dir=current_dir / "hospital", group_id="com.hospital", package_name="com.hospital" + ) + + files = generator.generate() if __name__ == "__main__": - proba_jednostavna_klasa() \ No newline at end of file + proba() \ No newline at end of file diff --git a/besser/generators/spring/spring_backend_generator.py b/besser/generators/spring/spring_backend_generator.py index b5eb6bc52..8bfc6cc3b 100644 --- a/besser/generators/spring/spring_backend_generator.py +++ b/besser/generators/spring/spring_backend_generator.py @@ -93,7 +93,7 @@ def _generate_main_and_test_files(self): templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") env = Environment(loader=FileSystemLoader(templates_path)) - file_path = self.build_generation_path(file_name=Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / f"{self.app_name.capitalize()}.java") + file_path = self.build_generation_path(file_name=Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / f"{self.app_name[0].upper() + self.app_name[1:]}.java") Path(file_path).parent.mkdir(parents=True, exist_ok=True) main_template = env.get_template("main.java.j2") @@ -106,7 +106,7 @@ def _generate_main_and_test_files(self): generated_code = main_template.render(**context) f.write(generated_code) - file_path = self.build_generation_path(file_name=Path("src", "test", "java") / Path().joinpath(*self.package_name.split(".")) / f"{self.app_name.capitalize()}Tests.java") + file_path = self.build_generation_path(file_name=Path("src", "test", "java") / Path().joinpath(*self.package_name.split(".")) / f"{self.app_name[0].upper() + self.app_name[1:]}Tests.java") Path(file_path).parent.mkdir(parents=True, exist_ok=True) test_template = env.get_template("test.java.j2") diff --git a/besser/generators/spring/spring_controller_generator.py b/besser/generators/spring/spring_controller_generator.py index 8b0b34e57..9282daa5b 100644 --- a/besser/generators/spring/spring_controller_generator.py +++ b/besser/generators/spring/spring_controller_generator.py @@ -4,11 +4,22 @@ from jinja2 import Environment, FileSystemLoader -from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, StringType, TimeDeltaType, TimeType +from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, Property, StringType, TimeDeltaType, TimeType from besser.generators.generator_interface import GeneratorInterface class SpringControllerGenerator(GeneratorInterface): + JAVA_TYPES = { + StringType.name: "String", + BooleanType.name: "Boolean", + IntegerType.name: "Integer", + FloatType.name: "Float", + DateType.name: "LocalDate", + DateTimeType.name: "LocalDateTime", + TimeType.name: "LocalTime", + TimeDeltaType.name: "Duration" + } + def __init__(self, model: DomainModel, entity_package_name: str, service_package_name: str, @@ -28,7 +39,7 @@ def generate(self): self._generate_controller_file(cls) def _generate_controller_file(self, cls: Class): - file_path = self.build_generation_path(file_name=f"{cls.name.capitalize()}Controller.java") + file_path = self.build_generation_path(file_name=f"{cls.name[0].upper() + cls.name[1:]}Controller.java") Path(file_path).parent.mkdir(parents=True, exist_ok=True) templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) @@ -47,13 +58,16 @@ def _generate_controller_file(self, cls: Class): imports.add("org.springframework.http.ResponseEntity") imports.add("java.util.Optional") imports.add("java.util.List") - imports.add(f"{self.service_package_name}.interfaces.I{cls.name.capitalize()}Service") + imports.add(f"{self.service_package_name}.interfaces.I{cls.name[0].upper() + cls.name[1:]}Service") imports.add(f"{self.entity_package_name}.{cls.name}") + id_attr: Property = next(attr for attr in cls.all_attributes() if attr.is_id) + context = { "package": f"{self.package_name}", "imports": sorted(imports), - "cls": cls.name + "cls": cls.name, + "id_type": self.JAVA_TYPES[id_attr.type.name] } with open(file_path, mode="w", encoding="utf-8") as f: diff --git a/besser/generators/spring/spring_entity_generator.py b/besser/generators/spring/spring_entity_generator.py index 1d6516339..7160af645 100644 --- a/besser/generators/spring/spring_entity_generator.py +++ b/besser/generators/spring/spring_entity_generator.py @@ -46,7 +46,7 @@ def generate(self): self._generate_class_file(cls, assoc_map[cls.name]) def _generate_class_file(self, cls: Class, assocs_for_class: List[BinaryAssociation]): - file_path = self.build_generation_path(file_name=f"{cls.name.capitalize()}.java") + file_path = self.build_generation_path(file_name=f"{cls.name[0].upper() + cls.name[1:]}.java") templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") env = Environment(loader=FileSystemLoader(templates_path)) entity_template = env.get_template("entity.java.j2") @@ -235,14 +235,14 @@ def _get_relation_owner_map(self, model: DomainModel) -> dict[str, str]: return ret def _generate_enum_file(self, enum: Enumeration): - file_path = self.build_generation_path(file_name=f"{enum.name.capitalize()}.java") + file_path = self.build_generation_path(file_name=f"{enum.name[0].upper() + enum.name[1:]}.java") templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") env = Environment(loader=FileSystemLoader(templates_path)) entity_template = env.get_template("enum.java.j2") context = { "package_name": self.package_name, - "name": enum.name.capitalize(), + "name": enum.name[0].upper() + enum.name[1:], "literals": enum.literals } diff --git a/besser/generators/spring/spring_repository_generator.py b/besser/generators/spring/spring_repository_generator.py index 4e551335d..e14a10c91 100644 --- a/besser/generators/spring/spring_repository_generator.py +++ b/besser/generators/spring/spring_repository_generator.py @@ -4,7 +4,7 @@ from jinja2 import Environment, FileSystemLoader -from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, StringType, TimeDeltaType, TimeType +from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, Property, StringType, TimeDeltaType, TimeType from besser.generators.generator_interface import GeneratorInterface class SpringRepositoryGenerator(GeneratorInterface): @@ -37,7 +37,7 @@ def generate(self): self._generate_repository_file(cls) def _generate_repository_file(self, cls: Class): - file_path = self.build_generation_path(file_name=f"I{cls.name.capitalize()}Repository.java") + file_path = self.build_generation_path(file_name=f"I{cls.name[0].upper() + cls.name[1:]}Repository.java") Path(file_path).parent.mkdir(parents=True, exist_ok=True) templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) @@ -64,7 +64,7 @@ def _generate_repository_file(self, cls: Class): method: object = {} method["return_value"] = f"ArrayList<{cls.name}>" - method["name"] = f"findAllBy{attr.name.capitalize()}" + method["name"] = f"findAllBy{attr.name[0].upper() + attr.name[1:]}" if is_enum or is_class: parameter_type = attr.type.name @@ -84,17 +84,20 @@ def _generate_repository_file(self, cls: Class): if attr.type.name in [DateType.name, DateTimeType.name, TimeType.name]: methods.append({ "return_value": f"ArrayList<{cls.name}>", - "name": f"findAllBy{attr.name.capitalize()}Between", + "name": f"findAllBy{attr.name[0].upper() + attr.name[1:]}Between", "parameter": f"{parameter_type} start, {parameter_type} end" }) methods.append(method) + id_attr: Property = next(attr for attr in cls.all_attributes() if attr.is_id) + context = { "package": f"{self.package_name}", "imports": sorted(imports), "cls": cls.name, - "methods": sorted(methods, key=lambda m: m["name"]) + "methods": sorted(methods, key=lambda m: m["name"]), + "id_type": self.JAVA_TYPES[id_attr.type.name] } with open(file_path, mode="w", encoding="utf-8") as f: diff --git a/besser/generators/spring/spring_service_generator.py b/besser/generators/spring/spring_service_generator.py index 2376b4994..a03af1d3f 100644 --- a/besser/generators/spring/spring_service_generator.py +++ b/besser/generators/spring/spring_service_generator.py @@ -4,7 +4,7 @@ from jinja2 import Environment, FileSystemLoader -from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, StringType, TimeDeltaType, TimeType +from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, Property, StringType, TimeDeltaType, TimeType from besser.generators.generator_interface import GeneratorInterface class SpringServiceGenerator(GeneratorInterface): @@ -42,7 +42,7 @@ def _generate_service_files(self, cls: Class): templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) - file_path = self.build_generation_path(file_name=Path("interfaces") / f"I{cls.name.capitalize()}Service.java") + file_path = self.build_generation_path(file_name=Path("interfaces") / f"I{cls.name[0].upper() + cls.name[1:]}Service.java") Path(file_path).parent.mkdir(parents=True, exist_ok=True) iservice_template = env.get_template("iservice.java.j2") @@ -67,7 +67,7 @@ def _generate_service_files(self, cls: Class): method: object = {} method["return_value"] = f"ArrayList<{cls.name}>" - method["name"] = f"findAllBy{attr.name.capitalize()}" + method["name"] = f"findAllBy{attr.name[0].upper() + attr.name[1:]}" if is_enum or is_class: parameter_type = attr.type.name @@ -87,7 +87,7 @@ def _generate_service_files(self, cls: Class): if attr.type.name in [DateType.name, DateTimeType.name, TimeType.name]: methods.append({ "return_value": f"ArrayList<{cls.name}>", - "name": f"findAllBy{attr.name.capitalize()}Between", + "name": f"findAllBy{attr.name[0].upper() + attr.name[1:]}Between", "parameter": f"{parameter_type} start, {parameter_type} end" }) @@ -106,7 +106,7 @@ def _generate_service_files(self, cls: Class): generated_code = iservice_template.render(**context) f.write(generated_code) - file_path = self.build_generation_path(file_name=Path("impl") / f"{cls.name.capitalize()}Service.java") + file_path = self.build_generation_path(file_name=Path("impl") / f"{cls.name[0].upper() + cls.name[1:]}Service.java") Path(file_path).parent.mkdir(parents=True, exist_ok=True) service_template = env.get_template("service.java.j2") @@ -114,8 +114,8 @@ def _generate_service_files(self, cls: Class): imports.add("org.springframework.beans.factory.annotation.Autowired") imports.add("org.springframework.stereotype.Service") - imports.add(f"{self.package_name}.interfaces.I{cls.name.capitalize()}Service") - imports.add(f"{self.repository_package_name}.I{cls.name.capitalize()}Repository") + imports.add(f"{self.package_name}.interfaces.I{cls.name[0].upper() + cls.name[1:]}Service") + imports.add(f"{self.repository_package_name}.I{cls.name[0].upper() + cls.name[1:]}Repository") context["imports"] = sorted(imports) @@ -129,6 +129,8 @@ def _generate_service_files(self, cls: Class): f.write(generated_code) def _get_crud_methods(self, cls: Class) -> List[object]: + id_attr: Property = next(attr for attr in cls.all_attributes() if attr.is_id) + return [ { "return_value": f"List<{cls.name}>", @@ -138,7 +140,7 @@ def _get_crud_methods(self, cls: Class) -> List[object]: { "return_value": f"Optional<{cls.name}>", "name": "findById", - "parameter": "Integer id" + "parameter": f"{self.JAVA_TYPES[id_attr.type.name]} {id_attr.name}" }, { "return_value": f"{cls.name}", diff --git a/besser/generators/spring/templates/controller.java.j2 b/besser/generators/spring/templates/controller.java.j2 index 0b58f21f8..3dbc87c54 100644 --- a/besser/generators/spring/templates/controller.java.j2 +++ b/besser/generators/spring/templates/controller.java.j2 @@ -17,7 +17,7 @@ public class {{ cls }}Controller { } @GetMapping("/{id}") - public ResponseEntity<{{ cls }}> getById(@PathVariable Integer id) { + public ResponseEntity<{{ cls }}> getById(@PathVariable {{ id_type }} id) { return _{{ cls[0] | lower + cls[1:] }}Service.findById(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } @@ -28,7 +28,7 @@ public class {{ cls }}Controller { } @PutMapping("/{id}") - public ResponseEntity<{{ cls }}> update(@PathVariable Integer id, @RequestBody {{ cls }} entity) { + public ResponseEntity<{{ cls }}> update(@PathVariable {{ id_type }} id, @RequestBody {{ cls }} entity) { Optional<{{ cls }}> existing = _{{ cls[0] | lower + cls[1:] }}Service.findById(id); @@ -43,7 +43,7 @@ public class {{ cls }}Controller { } @DeleteMapping("/{id}") - public ResponseEntity deleteById(@PathVariable Integer id) { + public ResponseEntity deleteById(@PathVariable {{ id_type }} id) { Optional<{{ cls }}> existing = _{{ cls[0] | lower + cls[1:] }}Service.findById(id); diff --git a/besser/generators/spring/templates/entity.java.j2 b/besser/generators/spring/templates/entity.java.j2 index 4b3345c8c..2b59166e6 100644 --- a/besser/generators/spring/templates/entity.java.j2 +++ b/besser/generators/spring/templates/entity.java.j2 @@ -90,11 +90,11 @@ public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent {% endfor -%} {% for attribute in attributes %} - public {{ attribute.type }} get{{ attribute.name | capitalize }}() { + public {{ attribute.type }} get{{ attribute.name[0] | upper + attribute.name[1:] }}() { return this.{{ attribute.name }}; } - public void set{{ attribute.name | capitalize }}({{ attribute.type }} {{ attribute.name }}) { + public void set{{ attribute.name[0] | upper + attribute.name[1:] }}({{ attribute.type }} {{ attribute.name }}) { this.{{ attribute.name }} = {{ attribute.name }}; } {% endfor %} diff --git a/besser/generators/spring/templates/entity_template.j2 b/besser/generators/spring/templates/entity_template.j2 deleted file mode 100644 index 908e414c2..000000000 --- a/besser/generators/spring/templates/entity_template.j2 +++ /dev/null @@ -1,314 +0,0 @@ -package {{ package_name }}; - -{% for import in imports %} -import {{ import }}; -{% endfor %} - -/** - * Entity klasa {{ class_name }} - * Generisano automatski od BESSER B-UML modela - * - * BESSER dokumentacija: - * https://besser.readthedocs.io/en/latest/api/BUML/metamodel/api_structural.html - * - * Baeldung JPA/Hibernate asocijacije: - * https://www.baeldung.com/jpa-hibernate-associations - * -{% if description %} - * Opis: {{ description }} -{% endif %} - * @author Spring Entity Generator - * @version 1.0 - */ -{% if is_abstract %} -@Entity -@Inheritance(strategy = InheritanceType.JOINED) -{% else %} -@Entity -{% endif %} -@Table(name = "{{ table_name }}"{% if unique_constraints %}, uniqueConstraints = { -{% for constraint in unique_constraints %} - @UniqueConstraint(name = "{{ constraint.name }}", columnNames = {{{ constraint.columns }}}){% if not loop.last %},{% endif %} -{% endfor %} -}{% endif %}) -public {% if is_abstract %}abstract {% endif %}class {{ class_name }}{% if parent %} extends {{ parent }}{% endif %} { - -{% for attribute in attributes %} -{% if attribute.is_id %} - /** - * Primarna ključna vrednost - jedinstveni identifikator entiteta - * Automatski se generiše pri kreiranju novog entiteta - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "{{ attribute.column_name }}") - {{ attribute.visibility }} {{ attribute.java_type }} {{ attribute.name }}; - -{% elif attribute.is_enum %} - /** - * Enumeracija - ograničeni skup mogućih vrednosti - * Mogućih vrednosti: {{ attribute.enum_values }} -{% if attribute.description %} * Opis: {{ attribute.description }} -{% endif %} -{% if attribute.is_readonly %} * Readonly: Da -{% endif %} -{% if attribute.is_derived %} * Izvedena vrednost: Da -{% endif %} - */ - @Column(name = "{{ attribute.column_name }}"{% if not attribute.nullable %}, nullable = false{% endif %}{% if attribute.length %}, length = {{ attribute.length }}{% endif %}) - @Enumerated(EnumType.STRING) - {{ attribute.visibility }} {{ attribute.java_type }} {{ attribute.name }}{% if attribute.default_value %} = {{ attribute.default_value }}{% endif %}; - -{% else %} - /** - * Атрибут: {{ attribute.name }} - * Tip: {{ attribute.java_type }} - * Obavezno: {% if attribute.nullable %}Ne{% else %}Da{% endif %} -{% if attribute.unique %} * Jedinstveno: Da -{% endif %} -{% if attribute.is_readonly %} * Readonly: Da -{% endif %} -{% if attribute.is_derived %} * Izvedena vrednost: Da -{% endif %} -{% if attribute.description %} * Opis: {{ attribute.description }} -{% endif %} - */ - @Column( - name = "{{ attribute.column_name }}" - {%- if not attribute.nullable %}, - nullable = false - {%- endif %} - {%- if attribute.unique %}, - unique = true - {%- endif %} - {%- if attribute.length %}, - length = {{ attribute.length }} - {%- endif %} - {%- if attribute.updatable != None %}, - updatable = {{ attribute.updatable|lower }} - {%- endif %} - {%- if attribute.insertable != None %}, - insertable = {{ attribute.insertable|lower }} - {%- endif %} - ) -{% if attribute.is_readonly %} - @org.hibernate.annotations.Immutable -{% endif %} - {{ attribute.visibility }} {{ attribute.java_type }} {{ attribute.name }}; - -{% endif %} -{% endfor %} - -{% for relation in relations %} -{% if relation.type == 'OneToOne' %} - /** - * OneToOne relacija - {{ relation.side_description }} - * Povezuje sa: {{ relation.target_class }} - * Fetch strategija: {{ relation.fetch_type }} - * Cascade akcije: {{ relation.cascade_types_str }} - * Navigable: {{ relation.is_navigable }} -{% if relation.description %} * Opis: {{ relation.description }} -{% endif %} - */ - @OneToOne( - fetch = FetchType.{{ relation.fetch_type }}, - cascade = {{% for cascade in relation.cascade_types %}CascadeType.{{ cascade }}{% if not loop.last %}, {% endif %}{% endfor %}} - {%- if relation.is_optional %}, - optional = true - {%- else %}, - optional = false - {%- endif %} - ) -{% if relation.is_owning_side %} - @JoinColumn( - name = "{{ relation.join_column_name }}", - referencedColumnName = "id", - nullable = {{ relation.is_optional|lower }}, - unique = true - ) -{% endif %} - {{ relation.visibility }} {{ relation.target_class }} {{ relation.field_name }}; - -{% elif relation.type == 'OneToMany' %} - /** - * OneToMany relacija - INVERSE SIDE - * Povezuje sa: {{ relation.target_class }} - * Broj stavki: Više (0..*) - * Fetch strategija: {{ relation.fetch_type }} - * Cascade akcije: {{ relation.cascade_types_str }} - * Navigable: {{ relation.is_navigable }} - * Mapped by: {{ relation.mapped_by }} -{% if relation.description %} * Opis: {{ relation.description }} -{% endif %} - */ - @OneToMany( - fetch = FetchType.{{ relation.fetch_type }}, - mappedBy = "{{ relation.mapped_by }}", - cascade = {{% for cascade in relation.cascade_types %}CascadeType.{{ cascade }}{% if not loop.last %}, {% endif %}{% endfor %}} - ) - {{ relation.visibility }} Set<{{ relation.target_class }}> {{ relation.plural_name }}; - -{% elif relation.type == 'ManyToOne' %} - /** - * ManyToOne relacija - OWNING SIDE - * Povezuje sa: {{ relation.target_class }} - * Fetch strategija: {{ relation.fetch_type }} - * Cascade akcije: {{ relation.cascade_types_str }} - * Navigable: {{ relation.is_navigable }} -{% if relation.description %} * Opis: {{ relation.description }} -{% endif %} - */ - @ManyToOne( - fetch = FetchType.{{ relation.fetch_type }}, - cascade = {{% for cascade in relation.cascade_types %}CascadeType.{{ cascade }}{% if not loop.last %}, {% endif %}{% endfor %}} - ) - @JoinColumn( - name = "{{ relation.join_column_name }}", - referencedColumnName = "id", - nullable = {{ relation.is_optional|lower }} - ) - {{ relation.visibility }} {{ relation.target_class }} {{ relation.field_name }}; - -{% elif relation.type == 'ManyToMany' %} - /** - * ManyToMany relacija - {% if relation.is_owning_side %}OWNING SIDE{% else %}INVERSE SIDE{% endif %} - * Povezuje sa: {{ relation.target_class }} - * Fetch strategija: {{ relation.fetch_type }} - * Cascade akcije: {{ relation.cascade_types_str }} - * Navigable: {{ relation.is_navigable }} -{% if not relation.is_owning_side %} * Mapped by: {{ relation.mapped_by }} -{% endif %} -{% if relation.description %} * Opis: {{ relation.description }} -{% endif %} - */ - @ManyToMany( - fetch = FetchType.{{ relation.fetch_type }}, - cascade = {{% for cascade in relation.cascade_types %}CascadeType.{{ cascade }}{% if not loop.last %}, {% endif %}{% endfor %}} - ) -{% if relation.is_owning_side %} - @JoinTable( - name = "{{ relation.join_table_name }}", - joinColumns = { - @JoinColumn(name = "{{ relation.source_join_column }}", referencedColumnName = "id") - }, - inverseJoinColumns = { - @JoinColumn(name = "{{ relation.target_join_column }}", referencedColumnName = "id") - } - ) -{% endif %} - {{ relation.visibility }} Set<{{ relation.target_class }}> {{ relation.plural_name }}; - -{% endif %} -{% endfor %} - - // ===================== KONSTRUKTORI ===================== - - /** - * Prazan konstruktor - obavezan za JPA/Hibernate - */ - public {{ class_name }}() { -{% for relation in relations %} -{% if relation.type in ['OneToMany', 'ManyToMany'] %} - this.{{ relation.plural_name }} = new HashSet<>(); -{% endif %} -{% endfor %} - } - -{% if constructor_params %} - /** - * Konstruktor sa svim atributima - * -{% for param in constructor_params_list %} - * @param {{ param.name }} {{ param.type }} -{% endfor %} - */ - public {{ class_name }}({{ constructor_params }}) { -{% for assignment in constructor_assignments %} - {{ assignment }} -{% endfor %} -{% for relation in relations %} -{% if relation.type in ['OneToMany', 'ManyToMany'] %} - this.{{ relation.plural_name }} = new HashSet<>(); -{% endif %} -{% endfor %} - } - -{% endif %} - - // ===================== GETTERI I SETTERI ===================== - -{% for getter_setter in getters_setters %} - /** - * Preuzmi vrednost {{ getter_setter.attribute_name }} - * - * @return {{ getter_setter.java_type }} - */ - {{ getter_setter.visibility }} {{ getter_setter.java_type }} get{{ getter_setter.method_name }}() { - return {{ getter_setter.attribute_name }}; - } - - /** - * Postavi vrednost {{ getter_setter.attribute_name }} - * - * @param {{ getter_setter.attribute_name }} {{ getter_setter.java_type }} - */ - {{ getter_setter.visibility }} void set{{ getter_setter.method_name }}({{ getter_setter.java_type }} {{ getter_setter.attribute_name }}) { - this.{{ getter_setter.attribute_name }} = {{ getter_setter.attribute_name }}; - } - -{% endfor %} - - // ===================== UTILITY METODE ===================== - - /** - * Vraća string reprezentaciju entiteta - * - * @return String reprezentacija - */ - @Override - public String toString() { - return "{{ class_name }}{" + - "id=" + id + -{% for attribute in attributes %} -{% if not attribute.is_id %} - ", {{ attribute.name }}='" + {{ attribute.name }} + '\'' + -{% endif %} -{% endfor %} - '}'; - } - - /** - * Poredi dva entiteta po ID-u - * Koristi ID jer je to jedinstven identifikator - * - * @param o Object za poređenje - * @return true ako su entiteti jednaki - */ - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - {{ class_name }} that = ({{ class_name }}) o; - - // Koristi ID za poređenje jer je to jedinstven identifikator - if (id == null || that.id == null) { - return false; - } - return id.equals(that.id); - } - - /** - * Generiše hash kod entiteta - * Koristi ID jer je to jedinstven identifikator - * - * @return hash kod - */ - @Override - public int hashCode() { - return id == null ? 0 : id.hashCode(); - } -} \ No newline at end of file diff --git a/besser/generators/spring/templates/irepository.java.j2 b/besser/generators/spring/templates/irepository.java.j2 index cb621436c..400e2ebfa 100644 --- a/besser/generators/spring/templates/irepository.java.j2 +++ b/besser/generators/spring/templates/irepository.java.j2 @@ -5,7 +5,7 @@ import {{ import }}; {% endfor %} @Repository -public interface I{{ cls }}Repository extends JpaRepository<{{ cls }}, Integer> { +public interface I{{ cls }}Repository extends JpaRepository<{{ cls }}, {{ id_type }}> { {% for method in methods %} {{ method.return_value }} {{ method.name }}({{ method.parameter }}); diff --git a/besser/generators/spring/templates/main.java.j2 b/besser/generators/spring/templates/main.java.j2 index 72b93be73..ae2970faa 100644 --- a/besser/generators/spring/templates/main.java.j2 +++ b/besser/generators/spring/templates/main.java.j2 @@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class {{ name | capitalize }} { +public class {{ name[0] | upper + name[1:] }} { public static void main(String[] args) { - SpringApplication.run({{ name | capitalize }}.class, args); + SpringApplication.run({{ name[0] | upper + name[1:] }}.class, args); } } \ No newline at end of file diff --git a/besser/generators/spring/templates/test.java.j2 b/besser/generators/spring/templates/test.java.j2 index 66512b022..959884ff1 100644 --- a/besser/generators/spring/templates/test.java.j2 +++ b/besser/generators/spring/templates/test.java.j2 @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class {{ name | capitalize }}Tests { +class {{ name[0] | upper + name[1:] }}Tests { @Test void contextLoads() { From 05c4ee7fdbff946d5331a368ad7d6be888c6b7f2 Mon Sep 17 00:00:00 2001 From: Milos Stojanovic Date: Tue, 8 Sep 2026 13:25:13 +0200 Subject: [PATCH 14/16] cleanup --- besser/generators/spring/endpoint.http | 27 - .../spring/{proba.py => example.py} | 0 besser/generators/spring/gh.py | 745 ------------------ 3 files changed, 772 deletions(-) delete mode 100644 besser/generators/spring/endpoint.http rename besser/generators/spring/{proba.py => example.py} (100%) delete mode 100644 besser/generators/spring/gh.py diff --git a/besser/generators/spring/endpoint.http b/besser/generators/spring/endpoint.http deleted file mode 100644 index ad9763297..000000000 --- a/besser/generators/spring/endpoint.http +++ /dev/null @@ -1,27 +0,0 @@ -### GET ALL -GET http://localhost:8080/api/computer -Accept: application/json - -### GET BY ID -GET http://localhost:8080/api/computer/1 -Accept: application/json - -### CREATE -POST http://localhost:8080/api/computer -Content-Type: application/json - -{ - "model": "MacBook Pro22" -} - -### UPDATE -PUT http://localhost:8080/api/computer/1 -Content-Type: application/json - -{ - "model": "ThinkPad X1" -} - -### DELETE -DELETE http://localhost:8080/api/computer/1 -Accept: application/json \ No newline at end of file diff --git a/besser/generators/spring/proba.py b/besser/generators/spring/example.py similarity index 100% rename from besser/generators/spring/proba.py rename to besser/generators/spring/example.py diff --git a/besser/generators/spring/gh.py b/besser/generators/spring/gh.py deleted file mode 100644 index ce687a33c..000000000 --- a/besser/generators/spring/gh.py +++ /dev/null @@ -1,745 +0,0 @@ -""" -Spring Entity Code Generator for BESSER B-UML Models -Generates Spring JPA Entity classes from BESSER structural models -With full support for JPA/Hibernate associations and BESSER metamodel features - -BESSER dokumentacija: -https://besser.readthedocs.io/en/latest/api/BUML/metamodel/api_structural.html - -Features: -- Apstraktne klase (abstract classes) -- Enumeracije (enumerations) -- ID atributi (@Id annotation) -- Navigable fields -- Opcioni atributi -- Visibility modifiers (public, private, protected) -- Readonly atributi (@Immutable) -- Izvedeni atributi (derived) -- Updatable/Insertable constraints -- Opisi (descriptions) i metadata -- Unique constraints -- Inheritance strategies - -Author: Generator for BESSER-PEARL/BESSER -""" - -from typing import Set, Dict, List, Optional -from pathlib import Path -import re -from enum import Enum -from jinja2 import Template, Environment, FileSystemLoader - -from besser.BUML.metamodel.structural.structural import ( - Class, Association, DomainModel, Property, Type, Multiplicity, Enumeration -) -from besser.generators.generator_interface import GeneratorInterface - - -class SpringEntityGenerator(GeneratorInterface): - """ - Generator za Spring Entity klase iz BESSER B-UML modela. - - Podržava sve BESSER metamodel koncepte: - - Apstraktne klase - - Enumeracije - - ID atributi - - Navigable polja - - Opcioni atributi - - Visibility modifiers - - Readonly atributi - - Izvedeni atributi - - Updatable/Insertable constraints - - Inheritance - - Sva JPA/Hibernate asocijacije - - Primer korišćenja: - ```python - from besser.BUML.metamodel.structural.structural import Diagram - from spring_entity_generator import SpringEntityGenerator - - diagram = Diagram() - # ... dodaj klase, atribute, relacije ... - - generator = SpringEntityGenerator( - diagram, - output_path="./src/main/java", - templates_path="./templates" - ) - generator.generate() - ``` - """ - - # Java tipovi mapiranje - JAVA_TYPE_MAPPING = { - 'String': 'String', - 'Integer': 'Integer', - 'Int': 'Integer', - 'Long': 'Long', - 'Double': 'Double', - 'Float': 'Float', - 'Boolean': 'Boolean', - 'Bool': 'Boolean', - 'LocalDate': 'LocalDate', - 'LocalDateTime': 'LocalDateTime', - 'Date': 'LocalDateTime', - 'BigDecimal': 'BigDecimal', - 'UUID': 'UUID', - } - - def __init__(self, model, output_path: str = "./generated/entities", - package_name: str = "com.example.entity", - templates_path: str = "./templates"): - """ - Inicijalizuj generator - - Args: - model: B-UML Diagram model - output_path: Putanja gde će biti sačuvane generirane klase - package_name: Java package za generirane klase - templates_path: Putanja do Jinja2 template fajlova - """ - super().__init__(model) - self.output_path = Path(output_path) - self.package_name = package_name - self.templates_path = Path(templates_path) - self.output_path.mkdir(parents=True, exist_ok=True) - - # Inicijalizuj Jinja2 okruženje - self.jinja_env = Environment( - loader=FileSystemLoader(str(self.templates_path)), - trim_blocks=True, - lstrip_blocks=True - ) - - # Mapiranje B-UML klasa sa njihovim asocijacijama - self.associations_by_class: Dict[str, List[Association]] = {} - self.enumerations: Dict[str, Enumeration] = {} - - self._build_association_map() - self._build_enumerations_map() - - def _build_association_map(self): - """Gradi mapu asocijacija za svaku klasu""" - if not hasattr(self.model, 'classes') or self.model.classes is None: - return - - for cls in self.model.classes: - self.associations_by_class[cls.name] = [] - - for cls in self.model.classes: - if hasattr(cls, 'association_ends') and cls.association_ends: - for assoc_end in cls.association_ends: - if hasattr(assoc_end, 'association'): - assoc = assoc_end.association - if assoc not in self.associations_by_class[cls.name]: - self.associations_by_class[cls.name].append(assoc) - - def _build_enumerations_map(self): - """Gradi mapu enumeracija""" - if not hasattr(self.model, 'enumerations') or self.model.enumerations is None: - return - - for enum in self.model.enumerations: - self.enumerations[enum.name] = enum - - def generate(self) -> List[str]: - """ - Generiši sve Spring Entity klase i enumeracije - - Returns: - Lista putanja generianih datoteka - """ - generated_files = [] - - # Generiši enumeracije - if hasattr(self.model, 'enumerations') and self.model.enumerations: - for enum in self.model.enumerations: - try: - file_path = self._generate_enumeration_class(enum) - generated_files.append(file_path) - except Exception as e: - print(f"Greška pri generisanju enumeracije {enum.name}: {e}") - - # Generiši entitete - if not hasattr(self.model, 'classes') or self.model.classes is None: - return generated_files - - for cls in self.model.classes: - try: - file_path = self._generate_entity_class(cls) - generated_files.append(file_path) - except Exception as e: - print(f"Greška pri generisanju klase {cls.name}: {e}") - - return generated_files - - def _generate_enumeration_class(self, enum: Enumeration) -> str: - """Generiši Java enumeraciju""" - enum_values = [] - - if hasattr(enum, 'literals') and enum.literals: - for literal in enum.literals: - enum_values.append(literal.name if hasattr(literal, 'name') else str(literal)) - - enum_code = f"""package {self.package_name}; - -/** - * Enumeracija {enum.name} - * - * Generirano automatski od BESSER B-UML modela - * - * @author Spring Entity Generator - * @version 1.0 - */ -public enum {enum.name} {{ -""" - - for i, value in enumerate(enum_values): - enum_code += f" {value}" - if i < len(enum_values) - 1: - enum_code += ",\n" - else: - enum_code += ";\n" - - enum_code += f""" - /** - * Pronađi enumeraciju po imenu - * - * @param value String vrednost - * @return {enum.name} enumeracija - */ - public static {enum.name} fromValue(String value) {{ - try {{ - return valueOf(value.toUpperCase()); - }} catch (IllegalArgumentException e) {{ - return null; - }} - }} -}} -""" - - # Napiši datoteku - file_path = self.output_path / f"{enum.name}.java" - with open(file_path, 'w', encoding='utf-8') as f: - f.write(enum_code) - - return str(file_path) - - def _generate_entity_class(self, buml_class: Class) -> str: - """Generiši jednu Spring Entity klasu""" - - # Pripremi podatke za template - context = { - 'class_name': buml_class.name, - 'package_name': self.package_name, - 'table_name': self._to_snake_case(buml_class.name), - 'parent': self._get_parent_class(buml_class), - 'is_abstract': self._is_abstract_class(buml_class), - 'description': self._get_description(buml_class), - 'imports': self._generate_imports(buml_class), - 'attributes': self._prepare_attributes(buml_class), - 'relations': self._prepare_relations(buml_class), - 'unique_constraints': self._prepare_unique_constraints(buml_class), - 'constructor_params': self._generate_constructor_params(buml_class), - 'constructor_params_list': self._generate_constructor_params_list(buml_class), - 'constructor_assignments': self._generate_constructor_assignments(buml_class), - 'getters_setters': self._prepare_getters_setters(buml_class), - } - - # Učitaj i primeni template - template = self.jinja_env.get_template('entity_template.j2') - entity_code = template.render(**context) - - # Napiši datoteku - file_path = self.output_path / f"{buml_class.name}.java" - with open(file_path, 'w', encoding='utf-8') as f: - f.write(entity_code) - - return str(file_path) - - def _is_abstract_class(self, buml_class: Class) -> bool: - """Proverite da li je klasa apstraktna""" - return hasattr(buml_class, 'is_abstract') and buml_class.is_abstract - - def _get_parent_class(self, buml_class: Class) -> Optional[str]: - """Pronađi parent klasu ako postoji generalizacija""" - if hasattr(buml_class, 'parents') and buml_class.parents: - parents = buml_class.parents - if parents: - parent = list(parents)[0] - return parent.name - return None - - def _get_description(self, buml_class: Class) -> Optional[str]: - """Preuzmi opis klase""" - if hasattr(buml_class, 'metadata') and buml_class.metadata: - if hasattr(buml_class.metadata, 'description'): - return buml_class.metadata.description - return None - - def _get_visibility(self, element) -> str: - """Preuzmi visibility modifier""" - visibility = "private" # Default - - if hasattr(element, 'visibility'): - vis = element.visibility - if vis is None: - visibility = "public" - elif isinstance(vis, str): - vis_lower = vis.lower() - if 'public' in vis_lower: - visibility = "public" - elif 'protected' in vis_lower: - visibility = "protected" - elif 'private' in vis_lower: - visibility = "private" - else: - # Ako je VisibilityKind enum - try: - if hasattr(vis, 'name'): - vis_name = vis.name.lower() - if 'public' in vis_name: - visibility = "public" - elif 'protected' in vis_name: - visibility = "protected" - elif 'private' in vis_name: - visibility = "private" - except: - pass - - return visibility - - def _generate_imports(self, buml_class: Class) -> List[str]: - """Generiši potrebne Java importe""" - imports = set([ - "jakarta.persistence.Entity", - "jakarta.persistence.Table", - "jakarta.persistence.Id", - "jakarta.persistence.GeneratedValue", - "jakarta.persistence.GenerationType", - "jakarta.persistence.Column", - "jakarta.persistence.OneToOne", - "jakarta.persistence.OneToMany", - "jakarta.persistence.ManyToOne", - "jakarta.persistence.ManyToMany", - "jakarta.persistence.JoinColumn", - "jakarta.persistence.JoinTable", - "jakarta.persistence.FetchType", - "jakarta.persistence.CascadeType", - "jakarta.persistence.Enumerated", - "jakarta.persistence.EnumType", - "jakarta.persistence.UniqueConstraint", - "java.time.LocalDate", - "java.time.LocalDateTime", - "java.util.HashSet", - "java.util.Set", - "java.util.Objects", - ]) - - # Dodaj inheritance import ako je potrebna - if self._is_abstract_class(buml_class): - imports.add("jakarta.persistence.Inheritance") - imports.add("jakarta.persistence.InheritanceType") - - # Dodaj importe na osnovu atributa - if buml_class.attributes: - for attr in buml_class.attributes: - if self._is_readonly_attribute(attr): - imports.add("org.hibernate.annotations.Immutable") - - if self._is_enum_attribute(attr): - enum_type = self._get_enum_name(attr.type) - imports.add(f"{self.package_name}.{enum_type}") - elif hasattr(attr.type, 'name'): - type_name = attr.type.name - if 'BigDecimal' in type_name: - imports.add("java.math.BigDecimal") - if 'UUID' in type_name: - imports.add("java.util.UUID") - - return sorted(list(imports)) - - def _is_readonly_attribute(self, attr: Property) -> bool: - """Proverite da li je atribut readonly""" - return hasattr(attr, 'is_read_only') and attr.is_read_only - - def _is_derived_attribute(self, attr: Property) -> bool: - """Proverite da li je atribut izvedena vrednost""" - return hasattr(attr, 'is_derived') and attr.is_derived - - def _is_enum_attribute(self, attr: Property) -> bool: - """Proverite da li je atribut enumeracija""" - if hasattr(attr, 'type') and attr.type: - type_name = attr.type.name if hasattr(attr.type, 'name') else str(attr.type) - return type_name in self.enumerations - return False - - def _get_enum_name(self, attr_type) -> str: - """Preuzmi naziv enumeracije""" - if hasattr(attr_type, 'name'): - return attr_type.name - return str(attr_type) - - def _prepare_attributes(self, buml_class: Class) -> List[Dict]: - """Pripremi podatke za atribute""" - attributes = [] - - if not buml_class.attributes: - return attributes - - for attr in buml_class.attributes: - if self._is_association_end(attr): - continue - - # Proverite da li je ovo ID polje - is_id = hasattr(attr, 'is_id') and attr.is_id - - # Visibility - visibility = self._get_visibility(attr) - - # Readonly - is_readonly = self._is_readonly_attribute(attr) - - # Derived - is_derived = self._is_derived_attribute(attr) - - # Enumeracija - is_enum = self._is_enum_attribute(attr) - - java_type = self._map_type_to_java(attr.type) - column_name = self._to_snake_case(attr.name) - - # Nullable - nullable = True - if hasattr(attr, 'lower') and attr.lower == 1: - nullable = False - - # Length - length = None - if java_type == 'String': - length = 255 - - # Unique - unique = False - if 'email' in attr.name.lower() or 'username' in attr.name.lower(): - unique = True - - # Opis - description = None - if hasattr(attr, 'metadata') and attr.metadata: - if hasattr(attr.metadata, 'description'): - description = attr.metadata.description - - # Default vrednost - default_value = None - if hasattr(attr, 'default_value'): - default_value = attr.default_value - - # Updatable/Insertable - updatable = None - insertable = None - - if is_readonly: - updatable = False - - if is_id or is_derived: - insertable = False - - # Enum vrednosti - enum_values = None - if is_enum: - enum_name = self._get_enum_name(attr.type) - if enum_name in self.enumerations: - enum = self.enumerations[enum_name] - if hasattr(enum, 'literals'): - enum_values = [lit.name if hasattr(lit, 'name') else str(lit) for lit in enum.literals] - - attributes.append({ - 'name': attr.name, - 'java_type': java_type, - 'column_name': column_name, - 'visibility': visibility, - 'nullable': nullable, - 'unique': unique, - 'length': length, - 'is_id': is_id, - 'is_enum': is_enum, - 'is_readonly': is_readonly, - 'is_derived': is_derived, - 'description': description, - 'default_value': default_value, - 'updatable': updatable, - 'insertable': insertable, - 'enum_values': ", ".join(enum_values) if enum_values else None - }) - - return attributes - - def _prepare_relations(self, buml_class: Class) -> List[Dict]: - """Pripremi podatke za relacije""" - relations = [] - - if buml_class.name not in self.associations_by_class: - return relations - - for assoc in self.associations_by_class[buml_class.name]: - source_end = None - target_end = None - - if hasattr(assoc, 'ends') and assoc.ends: - for end in assoc.ends: - if hasattr(end, 'type') and end.type == buml_class: - source_end = end - else: - target_end = end - - if not source_end or not target_end: - continue - - relation_type = self._determine_relation_type(source_end, target_end) - target_class = target_end.type.name if hasattr(target_end, 'type') else "Unknown" - field_name = self._to_camel_case_first_lower(target_class) - plural_name = self._pluralize(field_name) - plural_name_capitalized = self._capitalize_first(plural_name) - - # Visibility - visibility = self._get_visibility(source_end) - - # Navigable - is_navigable = hasattr(source_end, 'is_navigable') and source_end.is_navigable - - # Owning side - is_owning_side = relation_type in ["ManyToOne", "ManyToMany"] - - # Cascade types - cascade_types = ["PERSIST", "MERGE"] - if relation_type == "OneToMany": - cascade_types = ["ALL"] - - # Fetch type - fetch_type = "LAZY" - - # Optional - is_optional = True - if relation_type in ["ManyToOne"]: - if hasattr(target_end, 'lower') and target_end.lower == 1: - is_optional = False - - # Opis - description = None - if hasattr(source_end, 'metadata') and source_end.metadata: - if hasattr(source_end.metadata, 'description'): - description = source_end.metadata.description - - side_description = "OWNING SIDE" if is_owning_side else "INVERSE SIDE" - cascade_types_str = ", ".join(cascade_types) - - relation_data = { - 'type': relation_type, - 'target_class': target_class, - 'field_name': field_name, - 'plural_name': plural_name, - 'plural_name_capitalized': plural_name_capitalized, - 'join_column_name': self._to_snake_case(field_name) + '_id', - 'mapped_by': self._to_camel_case_first_lower(buml_class.name), - 'source_class': buml_class.name, - 'join_table_name': f"{self._to_snake_case(buml_class.name)}_{self._to_snake_case(target_class)}", - 'source_join_column': f"{self._to_snake_case(buml_class.name)}_id", - 'target_join_column': f"{self._to_snake_case(target_class)}_id", - 'visibility': visibility, - 'is_owning_side': is_owning_side, - 'is_navigable': is_navigable, - 'cascade_types': cascade_types, - 'cascade_types_str': cascade_types_str, - 'fetch_type': fetch_type, - 'is_optional': is_optional, - 'side_description': side_description, - 'description': description - } - - relations.append(relation_data) - - return relations - - def _prepare_unique_constraints(self, buml_class: Class) -> List[Dict]: - """Pripremi podatke za unique constraints""" - constraints = [] - - if not buml_class.attributes: - return constraints - - unique_attrs = [] - for attr in buml_class.attributes: - if self._is_association_end(attr): - continue - - if 'email' in attr.name.lower() or 'username' in attr.name.lower(): - unique_attrs.append(attr.name) - - if unique_attrs: - constraint = { - 'name': f"uk_{self._to_snake_case(buml_class.name)}_{'_'.join([self._to_snake_case(a) for a in unique_attrs])}", - 'columns': ', '.join([f'"{self._to_snake_case(a)}"' for a in unique_attrs]) - } - constraints.append(constraint) - - return constraints - - def _generate_constructor_params(self, buml_class: Class) -> str: - """Generiši parametre za konstruktor""" - all_attrs = self._get_all_attributes(buml_class) - - if not all_attrs: - return "" - - params = [] - for attr in all_attrs: - java_type = self._map_type_to_java(attr.type) - attr_name = attr.name if hasattr(attr, 'name') else "unknown" - params.append(f"{java_type} {attr_name}") - - return ", ".join(params) - - def _generate_constructor_params_list(self, buml_class: Class) -> List[Dict]: - """Generiši listu parametara sa tipom""" - all_attrs = self._get_all_attributes(buml_class) - params = [] - - for attr in all_attrs: - java_type = self._map_type_to_java(attr.type) - attr_name = attr.name if hasattr(attr, 'name') else "unknown" - params.append({ - 'name': attr_name, - 'type': java_type - }) - - return params - - def _generate_constructor_assignments(self, buml_class: Class) -> List[str]: - """Generiši dodele vrednosti u konstruktoru""" - all_attrs = self._get_all_attributes(buml_class) - assignments = [] - - for attr in all_attrs: - attr_name = attr.name if hasattr(attr, 'name') else "unknown" - assignments.append(f"this.{attr_name} = {attr_name};") - - return assignments - - def _get_all_attributes(self, buml_class: Class) -> List: - """Preuzmi sve atribute (i obične i relacijske)""" - all_attrs = [] - - if buml_class.attributes: - for attr in buml_class.attributes: - if not self._is_association_end(attr): - all_attrs.append(attr) - - if buml_class.name in self.associations_by_class: - for assoc in self.associations_by_class[buml_class.name]: - for end in assoc.ends: - if hasattr(end, 'type') and end.type != buml_class: - all_attrs.append(end) - break - - return all_attrs - - def _prepare_getters_setters(self, buml_class: Class) -> List[Dict]: - """Pripremi podatke za getter i setter metode""" - getters_setters = [] - - if not buml_class.attributes: - return getters_setters - - for attr in buml_class.attributes: - if self._is_association_end(attr): - continue - - java_type = self._map_type_to_java(attr.type) - attr_name = attr.name if hasattr(attr, 'name') else "unknown" - method_name = self._capitalize_first(attr_name) - visibility = self._get_visibility(attr) - - getters_setters.append({ - 'java_type': java_type, - 'attribute_name': attr_name, - 'method_name': method_name, - 'visibility': visibility - }) - - return getters_setters - - def _is_association_end(self, attr: Property) -> bool: - """Proverite da li je atribut asocijacijski kraj""" - if hasattr(attr, 'type') and attr.type: - type_name = attr.type.name if hasattr(attr.type, 'name') else str(attr.type) - return type_name[0].isupper() and type_name not in self.JAVA_TYPE_MAPPING - return False - - def _map_type_to_java(self, buml_type) -> str: - """Mapira B-UML tip na Java tip""" - if buml_type is None: - return "Object" - - type_name = buml_type.name if hasattr(buml_type, 'name') else str(buml_type) - - if type_name in self.enumerations: - return type_name - - return self.JAVA_TYPE_MAPPING.get(type_name, type_name) - - def _determine_relation_type(self, source_end, target_end) -> str: - """Odredi tip relacije na osnovu multipliciteta""" - source_mult = self._get_multiplicity(source_end) - target_mult = self._get_multiplicity(target_end) - - if target_mult in ["0..1", "1"]: - if source_mult in ["0..*", "*"]: - return "ManyToOne" - return "OneToOne" - elif target_mult in ["0..*", "*"]: - if source_mult in ["0..*", "*"]: - return "ManyToMany" - return "OneToMany" - - return "ManyToOne" - - def _get_multiplicity(self, end) -> str: - """Pronađi multipliciteta asocijacionog kraja""" - if hasattr(end, 'multiplicity') and end.multiplicity: - mult = end.multiplicity - if hasattr(mult, 'lower') and hasattr(mult, 'upper'): - if mult.upper == -1: - return f"{mult.lower}..*" - return f"{mult.lower}..{mult.upper}" - return "0..1" - - @staticmethod - def _to_snake_case(name: str) -> str: - """Konvertuje CamelCase u snake_case""" - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() - - @staticmethod - def _to_camel_case_first_lower(name: str) -> str: - """Konvertuje CamelCase u camelCase""" - if not name: - return name - return name[0].lower() + name[1:] - - @staticmethod - def _capitalize_first(name: str) -> str: - """Velikim baš prvi karakter""" - if not name: - return name - return name[0].upper() + name[1:] - - @staticmethod - def _pluralize(name: str) -> str: - """Pluralizuje ime""" - if name.endswith('y'): - return name[:-1] + 'ies' - elif name.endswith(('s', 'x', 'z', 'ch', 'sh')): - return name + 'es' - else: - return name + 's' \ No newline at end of file From 5e66dbd028f50f621963bfc312cadeec9d7c262e Mon Sep 17 00:00:00 2001 From: ArmSL Date: Tue, 22 Sep 2026 12:50:05 +0200 Subject: [PATCH 15/16] fix(spring): make the generator usable from the backend and emit code that compiles The generator could not be instantiated by the web editor at all: SpringBackendGenerator required spring_boot_version, java_version and app_name positionally before output_dir, while the router calls every generator as generator_class(model, output_dir=...). Those are now keyword arguments with defaults, matching the rest of besser/generators. The generated project also did not compile. Four copies of the B-UML to Java type map had drifted apart: TimeType mapped to LocalTime in the entity and controller generators but to LocalDateTime in the repository and service generators, so an entity field and the methods taking it disagreed on type. The map now lives once in java_types.py, and the findAllBy signature builder is shared so the two sides cannot drift again. Association ownership compared a Multiplicity object against the integer 1, which is always unequal, so every association resolved to the same end as owner and mappedBy was placed arbitrarily. Ownership is now decided from multiplicity.max against UNLIMITED_MAX_MULTIPLICITY, which also fixes self-associations emitting one end twice instead of both. B-UML names reached the filesystem unsanitized. NamedElement.name rejects spaces and hyphens but permits "..", "/" and "\", so a class named ../../evil was a valid model that wrote outside output_dir. Class, field, package and file names now go through Java identifier sanitization, and the new _generate_spring router branch resolves the project directory with the existing _safe_path helper rather than a raw os.path.join. The Maven wrapper shipped as three .j2 templates containing no Jinja tags at all, rendered into .mvn/ where Maven does not look for them. They are static files under resources/ now, copied to the project root with the executable bit and fixed line endings, and declared as package data so they ship in the wheel. Also: the generator is registered in SUPPORTED_GENERATORS and dispatched with its configuration rather than falling through to the generic path that ignored it; unmapped types fall back to Object instead of raising KeyError; a class with no identifier reports which class rather than a bare StopIteration; the controller uses the resolved identifier accessor instead of a hardcoded setId; enumeration literals and associations are sorted so output is deterministic; and example.py, a 206-line scratch script no other generator ships, is removed. Adds tests/generators/spring with 24 tests covering structure, content, determinism, name sanitization and config pass-through. Verified end to end by running mvnw compile on a generated project. --- besser/generators/spring/__init__.py | 21 +- besser/generators/spring/_sub_generator.py | 113 ++++ besser/generators/spring/example.py | 207 ------- besser/generators/spring/java_types.py | 237 ++++++++ .../maven-wrapper.properties} | 2 +- .../{templates/mvnw.j2 => resources/mvnw} | 0 .../mvnw.cmd.j2 => resources/mvnw.cmd} | 0 .../spring/spring_backend_generator.py | 287 +++++---- .../spring/spring_controller_generator.py | 111 ++-- .../spring/spring_entity_generator.py | 563 ++++++++++-------- .../spring/spring_http_generator.py | 40 +- .../spring/spring_repository_generator.py | 117 +--- .../spring/spring_service_generator.py | 172 ++---- .../spring/templates/controller.java.j2 | 22 +- .../spring/templates/entity.java.j2 | 132 ++-- .../generators/spring/templates/enum.java.j2 | 4 +- .../generators/spring/templates/http.http.j2 | 16 +- .../spring/templates/irepository.java.j2 | 8 +- .../spring/templates/iservice.java.j2 | 8 +- .../generators/spring/templates/main.java.j2 | 6 +- .../spring/templates/service.java.j2 | 13 +- .../generators/spring/templates/test.java.j2 | 4 +- .../backend/constants/constants.py | 15 + .../backend/routers/generation_router.py | 60 ++ docs/source/generators.rst | 5 + docs/source/generators/spring.rst | 159 +++++ setup.cfg | 3 + .../spring/test_spring_generator.py | 337 +++++++++++ .../backend/test_api_integration.py | 85 ++- 29 files changed, 1754 insertions(+), 993 deletions(-) create mode 100644 besser/generators/spring/_sub_generator.py delete mode 100644 besser/generators/spring/example.py create mode 100644 besser/generators/spring/java_types.py rename besser/generators/spring/{templates/maven-wrapper.properties.j2 => resources/maven-wrapper.properties} (67%) rename besser/generators/spring/{templates/mvnw.j2 => resources/mvnw} (100%) rename besser/generators/spring/{templates/mvnw.cmd.j2 => resources/mvnw.cmd} (100%) create mode 100644 docs/source/generators/spring.rst create mode 100644 tests/generators/spring/test_spring_generator.py diff --git a/besser/generators/spring/__init__.py b/besser/generators/spring/__init__.py index 4f77175d7..2d275179f 100644 --- a/besser/generators/spring/__init__.py +++ b/besser/generators/spring/__init__.py @@ -1 +1,20 @@ -from .spring_entity_generator import * \ No newline at end of file +"""Spring Boot backend generator. + +Only :class:`SpringBackendGenerator` is public: the entity, repository, service, +controller and HTTP writers are internal helpers it composes, and they are not +usable on their own. +""" + +from .spring_backend_generator import ( + DEFAULT_JAVA_VERSION, + DEFAULT_SPRING_APP_NAME, + DEFAULT_SPRING_BOOT_VERSION, + SpringBackendGenerator, +) + +__all__ = [ + "SpringBackendGenerator", + "DEFAULT_SPRING_BOOT_VERSION", + "DEFAULT_JAVA_VERSION", + "DEFAULT_SPRING_APP_NAME", +] diff --git a/besser/generators/spring/_sub_generator.py b/besser/generators/spring/_sub_generator.py new file mode 100644 index 000000000..23eeaa0f7 --- /dev/null +++ b/besser/generators/spring/_sub_generator.py @@ -0,0 +1,113 @@ +"""Internal base class shared by the Spring sub-generators. + +The entity, repository, service, controller and HTTP writers are *not* stand-alone +generators: they are never registered in ``SUPPORTED_GENERATORS`` and they only +make sense inside the directory layout that +:class:`~besser.generators.spring.spring_backend_generator.SpringBackendGenerator` +lays out. They therefore deliberately do not implement ``GeneratorInterface`` — +they are plain helpers composed by the public generator, in the same way +``WebAppGenerator`` composes its own writers. +""" + +import os +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader + +from besser.BUML.metamodel.structural import ( + Class, + DateTimeType, + DateType, + DomainModel, + Enumeration, + TimeType, +) +from besser.generators.spring.java_types import ( + is_many, + java_type_for, + java_type_import, + to_java_accessor_suffix, + to_java_class_name, + to_java_field_name, +) + +TEMPLATES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + +#: B-UML types that get an extra ``findAllByBetween`` range finder. +RANGE_FINDER_TYPES = (DateType.name, DateTimeType.name, TimeType.name) + + +def build_environment(**options) -> Environment: + """Environment: A Jinja2 environment bound to the Spring template folder.""" + return Environment(loader=FileSystemLoader(TEMPLATES_PATH), **options) + + +class SpringSubGenerator: + """Common plumbing for the Spring sub-generators (internal helper).""" + + def __init__(self, model: DomainModel, output_dir: str | Path): + self.model: DomainModel = model + self.output_dir: Path = Path(output_dir) + self.enumerations: set[Enumeration] = model.get_enumerations() + self.classes: list[Class] = model.classes_sorted_by_inheritance() + # Names of every user-defined type, used to tell a class/enum reference + # apart from a primitive type when mapping to Java. + self.model_type_names: set[str] = ( + {cls.name for cls in self.classes} | {enum.name for enum in self.enumerations} + ) + + def concrete_classes(self) -> list[Class]: + """list[Class]: The non-abstract classes, in a deterministic order.""" + return [cls for cls in self.classes if not cls.is_abstract] + + def derived_finder_methods(self, cls: Class, entity_package_name: str, + imports: set[str]) -> list[dict]: + """Build the ``findAllBy`` query methods derived from a class. + + The repository interface, the service interface and the service + implementation all render the very same list, so that the delegation in + the implementation always matches the repository signature. ``imports`` + is extended in place with whatever the signatures need. + """ + class_name = to_java_class_name(cls.name) + methods: list[dict] = [] + + for attr in sorted(cls.attributes, key=lambda a: a.name): + if is_many(attr.multiplicity) or attr.is_id: + continue + + parameter_type = java_type_for(attr.type, self.model_type_names) + if attr.type.name in self.model_type_names: + imports.add(f"{entity_package_name}.{parameter_type}") + type_import = java_type_import(parameter_type) + if type_import: + imports.add(type_import) + + finder_suffix = to_java_accessor_suffix(attr.name) + + if attr.type.name in RANGE_FINDER_TYPES: + methods.append({ + "return_value": f"ArrayList<{class_name}>", + "name": f"findAllBy{finder_suffix}Between", + "parameter": f"{parameter_type} start, {parameter_type} end", + }) + + methods.append({ + "return_value": f"ArrayList<{class_name}>", + "name": f"findAllBy{finder_suffix}", + "parameter": f"{parameter_type} {to_java_field_name(attr.name)}", + }) + + if methods: + imports.add("java.util.ArrayList") + + return sorted(methods, key=lambda method: method["name"]) + + def write(self, relative_path: str | Path, content: str) -> str: + """str: Write ``content`` under the output directory, creating parents.""" + file_path = self.output_dir / relative_path + file_path.parent.mkdir(parents=True, exist_ok=True) + # newline="\n" keeps the generated Java byte-identical across platforms. + with open(file_path, mode="w", encoding="utf-8", newline="\n") as file: + file.write(content) + return str(file_path) diff --git a/besser/generators/spring/example.py b/besser/generators/spring/example.py deleted file mode 100644 index 2d2747916..000000000 --- a/besser/generators/spring/example.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -Jednostavna proba Spring Entity Generatora - sa apsolutnom putanjom -""" - -from datetime import datetime -import sys -import os -from pathlib import Path - -from besser.BUML.metamodel.structural.structural import ( - BinaryAssociation, BooleanType, DateTimeType, DateType, DomainModel, Class, FloatType, Generalization, IntegerType, Method, MethodImplementationType, Multiplicity, Parameter, Property, StringType, TimeDeltaType, TimeType, Type, Association, Enumeration, EnumerationLiteral -) -from besser.generators.spring.spring_entity_generator import SpringEntityGenerator -from besser.generators.spring.spring_backend_generator import SpringBackendGenerator -from besser.utilities.web_modeling_editor.backend.services.converters.buml_to_json.class_diagram_converter import class_buml_to_json - -def proba_jednostavna_klasa(): - current_dir = Path(__file__).parent - output_dir = current_dir / "generated/entity" - output_dir.mkdir(exist_ok=True) - - role = Enumeration(name="Role", literals={EnumerationLiteral(name="USER"), EnumerationLiteral(name="ADMIN")}) - hand = Enumeration(name="Hand", literals={EnumerationLiteral(name="LEFT"), EnumerationLiteral(name="RIGHT")}) - - # address = Class(name="Address") - # address.add_attribute(Property(name="id", type=IntegerType, is_id=True)) - - user = Class(name="User") - user.add_attribute(Property(name="id", type=IntegerType, is_id=True)) - user.add_attribute(Property(name="email", type=StringType, is_optional=True, default_value="mikica")) - # user.add_attribute(Property(name="firstName", type=StringType, is_optional=True, default_value="John")) - user.add_attribute(Property(name="age", type=IntegerType, default_value=2)) - user.add_attribute(Property(name="role", type=role, visibility="private", default_value="ADMIN")) - user.add_attribute(Property(name="birthday", type=DateType, visibility="protected", default_value={ - "year": 2056, - "month": 10, - "day": 30, - "hour": 16, - "minute": 30, - "second": 27 - })) - user.add_attribute(Property(name="birthday2", type=TimeType, visibility="protected")) - # user.add_attribute(Property(name="hand", type=hand)) - user.add_attribute(Property(name="nicknames", type=FloatType, multiplicity=Multiplicity(0, "*"), default_value="1.2f, 3.2f")) - # user.add_attribute(Property(name="address", type=address, visibility="protected")) - - thing = Class(name="Thing", is_abstract=True) - - computer = Class(name="Computer") - computer.add_attribute(Property(name="id", type=IntegerType, is_id=True)) - computer.add_attribute(Property(name="model", type=StringType)) - - method1 = Method("destroyComputer", visibility="private", parameters=[ - Parameter("when", DateTimeType), - Parameter("how", StringType) - ]) - method2 = Method("makeComputer", visibility="private", parameters=[ - Parameter("when", role) - ], type=DateTimeType) - computer.add_method(method1) - computer.add_method(method2) - - gen1 = Generalization(general=thing, specific=computer) - # a = datetime(2026, 11, 20, 16, 13, 22) - # a. - # assoc = BinaryAssociation(name="onetoone", ends={ - # Property(name="computer", type=computer, multiplicity=Multiplicity(1, 1), is_navigable=True), - # Property(name="owner", type=user, multiplicity=Multiplicity(1, 1), is_navigable=False) - # }) - - assoc = BinaryAssociation(name="onetomany", ends={ - Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True), - Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True) - }) - - # assoc2 = BinaryAssociation(name="manytoone", ends={ - # Property(name="computerss", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), - # Property(name="ownerr", type=user, multiplicity=Multiplicity(1, 1), is_navigable=True) - # }) - - # assoc = BinaryAssociation(name="ManyToMany", ends={ - # Property(name="computers", type=computer, multiplicity=Multiplicity(0, "*"), is_navigable=True), - # Property(name="owners", type=user, multiplicity=Multiplicity(0, "*"), is_navigable=True) - # }) - - model = DomainModel(name="proba", types={user, thing, computer, role, hand}, generalizations={gen1}, associations={assoc}) - - # generator = SpringEntityGenerator( - # model, - # output_dir=output_dir, - # package_name="spring.entities" - # ) - - generator = SpringBackendGenerator( - model, "3.5.11", java_version="17", app_name="Example", output_dir=current_dir / "backend2", package_name="com.example.faculty" - ) - - files = generator.generate() - - # json_model = class_buml_to_json(model) - - # import requests - - # res = requests.post("http://localhost:9000/besser_api/generate-output", json={ - # "title": "spring backendaa", - # "config": { - # "spring_boot_version": "3.5.11", - # "java_version": "17", - # "app_name": "Transformers", - # "package_name": "com.transformers", - # "project_name": "trans" - # }, - # "model": { - # "elements": json_model["elements"], - # "relationships": json_model["relationships"] - # }, - # "generator": "spring" - # }) - - # if res.status_code == 200: - # with open("project.zip", "wb") as f: - # f.write(res.content) - - # print("ZIP sacuvan kao project.zip") - # else: - # print(res.status_code, res.text) - -def proba(): - # Classes - Patient = Class(name="Patient") - Medicine = Class(name="Medicine") - PatientFile = Class(name="PatientFile") - Person = Class(name="Person", is_abstract=True) - Doctor = Class(name="Doctor") - - # Patient class attributes and methods - Patient_patientId: Property = Property(name="patientId", type=StringType) - Patient.attributes={Patient_patientId} - - # Medicine class attributes and methods - Medicine_id: Property = Property(name="id", type=IntegerType, is_id=True) - Medicine_name: Property = Property(name="name", type=StringType) - Medicine_m_heal: Method = Method(name="heal", parameters={}, implementation_type=MethodImplementationType.NONE) - Medicine.attributes={Medicine_id, Medicine_name} - Medicine.methods={Medicine_m_heal} - - # PatientFile class attributes and methods - PatientFile_text: Property = Property(name="text", type=StringType, default_value="This is patient file template text.") - PatientFile_id: Property = Property(name="id", type=IntegerType, is_id=True) - PatientFile.attributes={PatientFile_id, PatientFile_text} - - # Person class attributes and methods - Person_id: Property = Property(name="id", type=IntegerType, is_id=True) - Person_name: Property = Property(name="name", type=StringType) - Person_birthday: Property = Property(name="birthday", type=DateType, is_optional=True) - Person.attributes={Person_birthday, Person_id, Person_name} - - # Doctor class attributes and methods - Doctor_isDirector: Property = Property(name="isDirector", type=BooleanType) - Doctor_calling: Property = Property(name="calling", type=StringType) - Doctor.attributes={Doctor_calling, Doctor_isDirector} - - # Relationships - person_medicine: BinaryAssociation = BinaryAssociation( - name="person_medicine", - ends={ - Property(name="patient", type=Patient, multiplicity=Multiplicity(0, 9999), is_navigable=False), - Property(name="consumes", type=Medicine, multiplicity=Multiplicity(0, 9999)) - } - ) - person_person_file: BinaryAssociation = BinaryAssociation( - name="person_person_file", - ends={ - Property(name="patientfile", type=PatientFile, multiplicity=Multiplicity(1, 1), is_navigable=False), - Property(name="owner", type=Patient, multiplicity=Multiplicity(1, 1)) - } - ) - Doctor_Patient: BinaryAssociation = BinaryAssociation( - name="Doctor_Patient", - ends={ - Property(name="doctors", type=Doctor, multiplicity=Multiplicity(1, 9999)), - Property(name="patients", type=Patient, multiplicity=Multiplicity(1, 9999)) - } - ) - - # Generalizations - gen_Patient_Person = Generalization(general=Person, specific=Patient) - gen_Doctor_Person = Generalization(general=Person, specific=Doctor) - - # Domain Model - domain_model = DomainModel( - name="Class_Diagram", - types={Patient, Medicine, PatientFile, Person, Doctor}, - associations={person_medicine, person_person_file, Doctor_Patient}, - generalizations={gen_Patient_Person, gen_Doctor_Person}, - metadata=None - ) - - current_dir = Path(__file__).parent - generator = SpringBackendGenerator( - domain_model, "3.5.11", java_version="17", app_name="Hospital", output_dir=current_dir / "hospital", group_id="com.hospital", package_name="com.hospital" - ) - - files = generator.generate() - -if __name__ == "__main__": - proba() \ No newline at end of file diff --git a/besser/generators/spring/java_types.py b/besser/generators/spring/java_types.py new file mode 100644 index 000000000..07558f24d --- /dev/null +++ b/besser/generators/spring/java_types.py @@ -0,0 +1,237 @@ +"""Shared Java type mapping and naming helpers for the Spring Boot generator. + +Every sub-generator of the Spring package (entity, repository, service, +controller) resolves B-UML types and B-UML names through this module, so that a +single attribute is never rendered with one Java type in the entity and another +one in the repository signature that reads it. + +The naming helpers also act as the security boundary of the generator: +``NamedElement`` only rejects whitespace and hyphens, so a model may legally +contain a class called ``../../evil``. Every B-UML name that ends up as a file +name, a Java type, a field or a SQL identifier is therefore funnelled through +:func:`to_java_identifier` (or one of its wrappers) first. +""" + +import re + +from besser.BUML.metamodel.structural import ( + UNLIMITED_MAX_MULTIPLICITY, + AnyType, + BooleanType, + Class, + DateTimeType, + DateType, + Enumeration, + FloatType, + IntegerType, + Multiplicity, + StringType, + TimeDeltaType, + TimeType, + Type, +) + +#: Single source of truth for the B-UML primitive type -> Java type mapping. +#: Shared by the entity, repository, service and controller generators so that +#: a ``time`` attribute is a ``LocalTime`` everywhere it appears. +JAVA_TYPES: dict[str, str] = { + StringType.name: "String", + BooleanType.name: "Boolean", + IntegerType.name: "Integer", + FloatType.name: "Float", + DateType.name: "LocalDate", + DateTimeType.name: "LocalDateTime", + TimeType.name: "LocalTime", + TimeDeltaType.name: "Duration", + AnyType.name: "Object", +} + +#: Java type used for any B-UML type that has no explicit mapping. +DEFAULT_JAVA_TYPE: str = "Object" + +#: ``java.*`` imports required by the Java types that are not in ``java.lang``. +JAVA_TYPE_IMPORTS: dict[str, str] = { + "LocalDate": "java.time.LocalDate", + "LocalDateTime": "java.time.LocalDateTime", + "LocalTime": "java.time.LocalTime", + "Duration": "java.time.Duration", +} + +#: Reserved words and literals that cannot be used as Java identifiers. +JAVA_KEYWORDS: frozenset[str] = frozenset({ + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", + "class", "const", "continue", "default", "do", "double", "else", "enum", + "extends", "final", "finally", "float", "for", "goto", "if", "implements", + "import", "instanceof", "int", "interface", "long", "native", "new", + "package", "private", "protected", "public", "return", "short", "static", + "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", + "transient", "try", "void", "volatile", "while", + "true", "false", "null", "_", +}) + +#: A Java package is a dot-separated list of lowercase identifiers. +JAVA_PACKAGE_PATTERN = re.compile(r"^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$") + +_ILLEGAL_IDENTIFIER_CHARS = re.compile(r"[^0-9A-Za-z_$]+") +_SNAKE_BOUNDARY_1 = re.compile(r"(.)([A-Z][a-z]+)") +_SNAKE_BOUNDARY_2 = re.compile(r"([a-z0-9])([A-Z])") + +#: Fallback used when a name sanitizes down to nothing at all (e.g. ``"../.."``). +_FALLBACK_IDENTIFIER = "Unnamed" + + +def to_java_identifier(name: str, capitalize: bool = False) -> str: + """Turn an arbitrary B-UML name into a legal Java identifier. + + Every character that Java does not accept in an identifier is replaced by an + underscore, which is what stops path separators and ``..`` segments from + escaping the output directory. Identifiers that would start with a digit are + prefixed with an underscore, and Java keywords get a trailing underscore. + + Args: + name: The B-UML name to sanitize. + capitalize: Whether the first character should be upper-cased + (used for type names). + + Returns: + A legal, non-empty Java identifier. + """ + cleaned = _ILLEGAL_IDENTIFIER_CHARS.sub("_", str(name)) + # Underscores introduced by the substitution at the edges carry no meaning + # (``../../evil`` -> ``_evil``) and only make the generated code noisier. + cleaned = cleaned.strip("_") + if not cleaned: + cleaned = _FALLBACK_IDENTIFIER + if cleaned[0].isdigit(): + cleaned = f"_{cleaned}" + if capitalize: + # Capitalizing first is what makes a class called "new" legal as "New"; + # only what is left after it can still collide with a keyword. + cleaned = cleaned[0].upper() + cleaned[1:] + if cleaned in JAVA_KEYWORDS: + cleaned = f"{cleaned}_" + return cleaned + + +def to_java_class_name(name: str) -> str: + """str: The sanitized, capitalized Java type name for a B-UML name.""" + return to_java_identifier(name, capitalize=True) + + +def to_java_field_name(name: str) -> str: + """str: The sanitized Java field/parameter name for a B-UML name.""" + return to_java_identifier(name) + + +def to_java_accessor_suffix(name: str) -> str: + """str: The capitalized suffix of the ``getX``/``setX`` pair for a field.""" + field = to_java_field_name(name) + return field[0].upper() + field[1:] + + +def to_snake_case(name: str) -> str: + """str: The sanitized ``snake_case`` form of a name (column/table names).""" + sanitized = to_java_identifier(name) + partial = _SNAKE_BOUNDARY_1.sub(r"\1_\2", sanitized) + return _SNAKE_BOUNDARY_2.sub(r"\1_\2", partial).lower() + + +def pluralize(name: str) -> str: + """str: A naive English plural, used for ``@Table`` names.""" + if name.endswith("y"): + return name[:-1] + "ies" + if name.endswith(("s", "x", "z", "ch", "sh")): + return name + "es" + return name + "s" + + +#: B-UML visibility -> Java access modifier. ``package`` is the default access +#: in Java and has no keyword, so it maps to the empty modifier. +JAVA_VISIBILITY: dict[str, str] = { + "public": "public", + "private": "private", + "protected": "protected", + "package": "", +} + + +def java_visibility(visibility: str) -> str: + """str: The Java access modifier for a B-UML visibility.""" + return JAVA_VISIBILITY.get(visibility, "public") + + +def validate_java_package(package_name: str) -> str: + """Validate a Java package name and return it unchanged. + + The package name is split into directories, so an unvalidated value would be + a second way out of the output directory. + + Raises: + ValueError: If the package name is not a dot-separated list of lowercase + Java identifiers, or if one of its segments is a Java keyword. + """ + if not isinstance(package_name, str) or not JAVA_PACKAGE_PATTERN.match(package_name): + raise ValueError( + f"Invalid Java package name: {package_name!r}. A package name must be a " + "dot-separated list of lowercase identifiers, e.g. 'com.example.app'." + ) + for segment in package_name.split("."): + if segment in JAVA_KEYWORDS: + raise ValueError( + f"Invalid Java package name: {package_name!r}. " + f"'{segment}' is a reserved Java keyword." + ) + return package_name + + +def java_type_for(buml_type: Type, model_type_names: set[str] | None = None) -> str: + """Map a B-UML type to its Java counterpart. + + User-defined types (classes and enumerations) keep their own, sanitized + name. Primitive types are looked up in :data:`JAVA_TYPES`, and anything the + mapping does not know about degrades to ``Object`` rather than raising. + + Args: + buml_type: The B-UML type to map. + model_type_names: Names of the classes and enumerations of the model. + Used so that a type referenced by name only is still recognised as + user-defined. + """ + if buml_type is None: + return "void" + name = buml_type.name + if isinstance(buml_type, (Class, Enumeration)) or (model_type_names and name in model_type_names): + return to_java_class_name(name) + return JAVA_TYPES.get(name, DEFAULT_JAVA_TYPE) + + +def java_type_import(java_type_name: str) -> str | None: + """str | None: The import required by a Java type name, if any.""" + return JAVA_TYPE_IMPORTS.get(java_type_name) + + +def is_many(multiplicity: Multiplicity | None) -> bool: + """bool: Whether a multiplicity denotes "many" (an upper bound above one). + + ``*`` is stored as :data:`UNLIMITED_MAX_MULTIPLICITY` by the metamodel, so + both the unbounded and the explicitly bounded cases are covered here. + """ + if multiplicity is None: + return False + return multiplicity.max == UNLIMITED_MAX_MULTIPLICITY or multiplicity.max > 1 + + +def get_id_attribute(cls: Class): + """Property: The identifier attribute of a class, inherited ones included. + + Raises: + ValueError: If the class has no attribute flagged with ``is_id``. + """ + id_attributes = [attr for attr in cls.all_attributes() if attr.is_id] + if not id_attributes: + raise ValueError( + f"Class '{cls.name}' has no identifier attribute. The Spring generator maps " + "every concrete class to a JPA entity, which requires exactly one attribute " + "marked with 'is_id=True' (directly or inherited)." + ) + return sorted(id_attributes, key=lambda attr: attr.name)[0] diff --git a/besser/generators/spring/templates/maven-wrapper.properties.j2 b/besser/generators/spring/resources/maven-wrapper.properties similarity index 67% rename from besser/generators/spring/templates/maven-wrapper.properties.j2 rename to besser/generators/spring/resources/maven-wrapper.properties index da4c7fc43..8dea6c227 100644 --- a/besser/generators/spring/templates/maven-wrapper.properties.j2 +++ b/besser/generators/spring/resources/maven-wrapper.properties @@ -1,3 +1,3 @@ wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip \ No newline at end of file +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/besser/generators/spring/templates/mvnw.j2 b/besser/generators/spring/resources/mvnw similarity index 100% rename from besser/generators/spring/templates/mvnw.j2 rename to besser/generators/spring/resources/mvnw diff --git a/besser/generators/spring/templates/mvnw.cmd.j2 b/besser/generators/spring/resources/mvnw.cmd similarity index 100% rename from besser/generators/spring/templates/mvnw.cmd.j2 rename to besser/generators/spring/resources/mvnw.cmd diff --git a/besser/generators/spring/spring_backend_generator.py b/besser/generators/spring/spring_backend_generator.py index 8bfc6cc3b..2911470e2 100644 --- a/besser/generators/spring/spring_backend_generator.py +++ b/besser/generators/spring/spring_backend_generator.py @@ -1,36 +1,81 @@ +"""Generates a complete, buildable Spring Boot backend from a B-UML domain model.""" + import os +import shutil +import stat from pathlib import Path -from jinja2 import Environment, FileSystemLoader - -from besser.BUML.metamodel.structural.structural import DomainModel +from besser.BUML.metamodel.structural import DomainModel from besser.generators.generator_interface import GeneratorInterface +from besser.generators.spring._sub_generator import build_environment +from besser.generators.spring.java_types import to_java_class_name, validate_java_package from besser.generators.spring.spring_controller_generator import SpringControllerGenerator from besser.generators.spring.spring_entity_generator import SpringEntityGenerator from besser.generators.spring.spring_http_generator import SpringHttpGenerator from besser.generators.spring.spring_repository_generator import SpringRepositoryGenerator from besser.generators.spring.spring_service_generator import SpringServiceGenerator +#: Defaults for the generated project. They live here rather than in the web +#: editor's constants module so that the generator never has to import the web +#: backend; ``backend/constants/constants.py`` re-exports these instead. +DEFAULT_SPRING_BOOT_VERSION: str = "3.4.4" +DEFAULT_JAVA_VERSION: str = "21" +DEFAULT_SPRING_APP_NAME: str = "Application" +DEFAULT_SPRING_PACKAGE_NAME: str = "com.example" +DEFAULT_SPRING_GROUP_ID: str = "com.example" + +#: Static (non-templated) files shipped with the generator. +RESOURCES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources") + + class SpringBackendGenerator(GeneratorInterface): - - def __init__(self, - model: DomainModel, - spring_boot_version: str, - java_version: str, - app_name: str, - output_dir: str, - package_name: str = "com.example", - group_id: str = "com.example", - description: str = "" - ): + """Generates a Spring Boot project (Maven, JPA entities, repositories, + services and REST controllers) from a B-UML domain model. + + Args: + model (DomainModel): The domain model to generate the backend from. + output_dir (str): The directory the project is written to. + spring_boot_version (str): The ``spring-boot-starter-parent`` version. + java_version (str): The ``java.version`` property of the generated POM. + app_name (str): The name of the application (and of its main class). + package_name (str): The root Java package of the generated sources. + group_id (str): The Maven ``groupId`` of the generated project. + description (str): The Maven ``description`` of the generated project. + """ + + def __init__(self, + model: DomainModel, + output_dir: str = None, + *, + spring_boot_version: str = DEFAULT_SPRING_BOOT_VERSION, + java_version: str = DEFAULT_JAVA_VERSION, + app_name: str = DEFAULT_SPRING_APP_NAME, + package_name: str = DEFAULT_SPRING_PACKAGE_NAME, + group_id: str = DEFAULT_SPRING_GROUP_ID, + description: str = ""): super().__init__(model, output_dir) - self.package_name = package_name - self.spring_boot_version = spring_boot_version - self.java_version = java_version - self.app_name = app_name - self.group_id = group_id - self.description = description + self.package_name: str = validate_java_package(package_name) + self.spring_boot_version: str = spring_boot_version + self.java_version: str = java_version + self.app_name: str = app_name + self.group_id: str = group_id + self.description: str = description + + @property + def project_dir(self) -> Path: + """Path: The root of the generated project.""" + return Path(self.build_generation_dir()) + + @property + def main_class_name(self) -> str: + """str: The sanitized name of the generated ``@SpringBootApplication``.""" + return to_java_class_name(self.app_name) + + @property + def package_dir(self) -> Path: + """Path: The relative path of the root package inside a source folder.""" + return Path(*self.package_name.split(".")) def generate(self): self._generate_pom_file() @@ -43,142 +88,126 @@ def generate(self): self._generate_controllers() self._generate_http() - def _generate_pom_file(self): - file_path = self.build_generation_path(file_name="pom.xml") - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path)) - pom_template = env.get_template("pom.xml.j2") - - context = { - "name": self.app_name, - "spring_boot_version": self.spring_boot_version, - "group_id": self.group_id, - "description": self.description, - "java_version": self.java_version - } - - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = pom_template.render(**context) - f.write(generated_code) - - def _generate_mvn_files(self): - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path)) - - file_path = self.build_generation_path(file_name=Path(self.output_dir, ".mvn", "wrapper", "maven-wrapper.properties")) - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - maven_wrapper_template = env.get_template("maven-wrapper.properties.j2") + # ------------------------------------------------------------------ + # Project scaffolding + # ------------------------------------------------------------------ - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = maven_wrapper_template.render() - f.write(generated_code) + def _render(self, template_name: str, relative_path: Path | str, **context): + env = build_environment() + content = env.get_template(template_name).render(**context) + file_path = self.project_dir / relative_path + file_path.parent.mkdir(parents=True, exist_ok=True) + with open(file_path, mode="w", encoding="utf-8", newline="\n") as file: + file.write(content) - file_path = self.build_generation_path(file_name=Path(self.output_dir, ".mvn", "mvnw")) - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - mvnw_template = env.get_template("mvnw.j2") - - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = mvnw_template.render() - f.write(generated_code) + def _generate_pom_file(self): + self._render( + "pom.xml.j2", "pom.xml", + name=self.app_name, + spring_boot_version=self.spring_boot_version, + group_id=self.group_id, + description=self.description, + java_version=self.java_version, + ) - file_path = self.build_generation_path(file_name=Path(self.output_dir, ".mvn", "mvnw.cmd")) - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - mvnw_cmd_template = env.get_template("mvnw.cmd.j2") + def _generate_mvn_files(self): + """Copy the Maven wrapper to the layout Maven expects. + + ``mvnw`` and ``mvnw.cmd`` have to sit at the project root; only + ``maven-wrapper.properties`` belongs under ``.mvn/wrapper/``. The two + scripts carry no model-dependent content, so they are plain static + resources rather than templates. + """ + project_dir = self.project_dir + project_dir.mkdir(parents=True, exist_ok=True) + + wrapper_dir = project_dir / ".mvn" / "wrapper" + wrapper_dir.mkdir(parents=True, exist_ok=True) + shutil.copy( + os.path.join(RESOURCES_PATH, "maven-wrapper.properties"), + wrapper_dir / "maven-wrapper.properties", + ) - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = mvnw_cmd_template.render() - f.write(generated_code) + # The line endings are forced on copy: a CRLF ``mvnw`` is unusable on + # POSIX ("bad interpreter"), whatever the checkout settings were. + self._copy_script("mvnw", project_dir / "mvnw", newline=b"\n", executable=True) + self._copy_script("mvnw.cmd", project_dir / "mvnw.cmd", newline=b"\r\n") + + @staticmethod + def _copy_script(resource_name: str, destination: Path, newline: bytes, executable: bool = False): + content = Path(RESOURCES_PATH, resource_name).read_bytes().replace(b"\r\n", b"\n") + if newline != b"\n": + content = content.replace(b"\n", newline) + destination.write_bytes(content) + if executable: + mode = os.stat(destination).st_mode + os.chmod(destination, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) def _generate_main_and_test_files(self): - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path)) - - file_path = self.build_generation_path(file_name=Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / f"{self.app_name[0].upper() + self.app_name[1:]}.java") - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - main_template = env.get_template("main.java.j2") - - context = { - "name": self.app_name, - "package": self.package_name - } - - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = main_template.render(**context) - f.write(generated_code) - - file_path = self.build_generation_path(file_name=Path("src", "test", "java") / Path().joinpath(*self.package_name.split(".")) / f"{self.app_name[0].upper() + self.app_name[1:]}Tests.java") - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - test_template = env.get_template("test.java.j2") - - context = { - "name": self.app_name, - "package": self.package_name - } - - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = test_template.render(**context) - f.write(generated_code) + self._render( + "main.java.j2", + Path("src", "main", "java") / self.package_dir / f"{self.main_class_name}.java", + name=self.main_class_name, + package=self.package_name, + ) + self._render( + "test.java.j2", + Path("src", "test", "java") / self.package_dir / f"{self.main_class_name}Tests.java", + name=self.main_class_name, + package=self.package_name, + ) def _generate_properties_file(self): - file_path = self.build_generation_path(file_name=Path("src", "main", "resources", "application.properties")) - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path)) - app_properties_template = env.get_template("application.properties.j2") + self._render( + "application.properties.j2", + Path("src", "main", "resources", "application.properties"), + name=self.app_name, + ) - context = { - "name": self.app_name, - } + # ------------------------------------------------------------------ + # Sources + # ------------------------------------------------------------------ - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = app_properties_template.render(**context) - f.write(generated_code) + def _source_dir(self, sub_package: str) -> Path: + return self.project_dir / Path("src", "main", "java") / self.package_dir / sub_package def _generate_entities(self): - generator = SpringEntityGenerator( + SpringEntityGenerator( self.model, - output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "entity", - package_name=f"{self.package_name}.entity" - ) - - generator.generate() + output_dir=self._source_dir("entity"), + package_name=f"{self.package_name}.entity", + ).generate() def _generate_repositories(self): - generator = SpringRepositoryGenerator( + SpringRepositoryGenerator( self.model, f"{self.package_name}.entity", - output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "repository", - package_name=f"{self.package_name}.repository" - ) - - generator.generate() + output_dir=self._source_dir("repository"), + package_name=f"{self.package_name}.repository", + ).generate() def _generate_services(self): - generator = SpringServiceGenerator( + SpringServiceGenerator( self.model, f"{self.package_name}.entity", f"{self.package_name}.repository", - output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "service", - package_name=f"{self.package_name}.service" - ) - - generator.generate() + output_dir=self._source_dir("service"), + package_name=f"{self.package_name}.service", + ).generate() def _generate_controllers(self): - generator = SpringControllerGenerator( + SpringControllerGenerator( self.model, f"{self.package_name}.entity", f"{self.package_name}.service", - output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "controller", - package_name=f"{self.package_name}.controller" - ) - - generator.generate() + output_dir=self._source_dir("controller"), + package_name=f"{self.package_name}.controller", + ).generate() def _generate_http(self): - generator = SpringHttpGenerator( + # Scratch request files are test resources, not compilation units: they + # must stay out of ``src/main/java``. + SpringHttpGenerator( self.model, - output_dir=self.output_dir / Path("src", "main", "java") / Path().joinpath(*self.package_name.split(".")) / "http_test" - ) - - generator.generate() \ No newline at end of file + output_dir=self.project_dir / Path("src", "test", "resources", "http"), + ).generate() diff --git a/besser/generators/spring/spring_controller_generator.py b/besser/generators/spring/spring_controller_generator.py index 9282daa5b..5119f9151 100644 --- a/besser/generators/spring/spring_controller_generator.py +++ b/besser/generators/spring/spring_controller_generator.py @@ -1,75 +1,74 @@ -import os -from pathlib import Path -from typing import List +"""Writes the REST controllers of a generated Spring Boot project.""" -from jinja2 import Environment, FileSystemLoader +from besser.BUML.metamodel.structural import Class, DomainModel +from besser.generators.spring._sub_generator import SpringSubGenerator, build_environment +from besser.generators.spring.java_types import ( + get_id_attribute, + java_type_for, + java_type_import, + to_java_accessor_suffix, + to_java_class_name, + to_java_field_name, + validate_java_package, +) -from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, Property, StringType, TimeDeltaType, TimeType -from besser.generators.generator_interface import GeneratorInterface -class SpringControllerGenerator(GeneratorInterface): +class SpringControllerGenerator(SpringSubGenerator): + """Generates the ``controller`` package of a Spring Boot project (internal helper).""" - JAVA_TYPES = { - StringType.name: "String", - BooleanType.name: "Boolean", - IntegerType.name: "Integer", - FloatType.name: "Float", - DateType.name: "LocalDate", - DateTimeType.name: "LocalDateTime", - TimeType.name: "LocalTime", - TimeDeltaType.name: "Duration" - } - - def __init__(self, model: DomainModel, + def __init__(self, model: DomainModel, entity_package_name: str, service_package_name: str, - output_dir: str = "./generated/controller", + output_dir: str = "./generated/controller", package_name: str = "com.example.controller"): super().__init__(model, output_dir) - self.package_name: str = package_name - self.entity_package_name: str = entity_package_name - self.service_package_name: str = service_package_name - self.enumerations: set[Enumeration] = model.get_enumerations() - self.classes: set[Class] = model.classes_sorted_by_inheritance() + self.package_name: str = validate_java_package(package_name) + self.entity_package_name: str = validate_java_package(entity_package_name) + self.service_package_name: str = validate_java_package(service_package_name) def generate(self): - for cls in self.classes: - if not cls.is_abstract: - self._generate_controller_file(cls) + for cls in self.concrete_classes(): + self._generate_controller_file(cls) def _generate_controller_file(self, cls: Class): - file_path = self.build_generation_path(file_name=f"{cls.name[0].upper() + cls.name[1:]}Controller.java") - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) - repository_template = env.get_template("controller.java.j2") + class_name: str = to_java_class_name(cls.name) + env = build_environment(trim_blocks=True, lstrip_blocks=True) + controller_template = env.get_template("controller.java.j2") + + imports: set[str] = { + "org.springframework.beans.factory.annotation.Autowired", + "org.springframework.web.bind.annotation.RestController", + "org.springframework.web.bind.annotation.RequestMapping", + "org.springframework.web.bind.annotation.PathVariable", + "org.springframework.web.bind.annotation.RequestBody", + "org.springframework.web.bind.annotation.GetMapping", + "org.springframework.web.bind.annotation.PostMapping", + "org.springframework.web.bind.annotation.PutMapping", + "org.springframework.web.bind.annotation.DeleteMapping", + "org.springframework.http.ResponseEntity", + "java.util.Optional", + "java.util.List", + f"{self.service_package_name}.interfaces.I{class_name}Service", + f"{self.entity_package_name}.{class_name}", + } - imports: set[str] = set() - imports.add("org.springframework.beans.factory.annotation.Autowired") - imports.add("org.springframework.web.bind.annotation.RestController") - imports.add("org.springframework.web.bind.annotation.RequestMapping") - imports.add("org.springframework.web.bind.annotation.PathVariable") - imports.add("org.springframework.web.bind.annotation.RequestBody") - imports.add("org.springframework.web.bind.annotation.GetMapping") - imports.add("org.springframework.web.bind.annotation.PostMapping") - imports.add("org.springframework.web.bind.annotation.PutMapping") - imports.add("org.springframework.web.bind.annotation.DeleteMapping") - imports.add("org.springframework.http.ResponseEntity") - imports.add("java.util.Optional") - imports.add("java.util.List") - imports.add(f"{self.service_package_name}.interfaces.I{cls.name[0].upper() + cls.name[1:]}Service") - imports.add(f"{self.entity_package_name}.{cls.name}") + id_attr = get_id_attribute(cls) + id_type: str = java_type_for(id_attr.type, self.model_type_names) + type_import = java_type_import(id_type) + if type_import: + imports.add(type_import) - id_attr: Property = next(attr for attr in cls.all_attributes() if attr.is_id) - context = { - "package": f"{self.package_name}", + "package": self.package_name, "imports": sorted(imports), - "cls": cls.name, - "id_type": self.JAVA_TYPES[id_attr.type.name] + "cls": class_name, + "instance": to_java_field_name(class_name[0].lower() + class_name[1:]), + "route": class_name[0].lower() + class_name[1:], + "id_type": id_type, + # The identifier is not necessarily called "id": the setter has to + # follow whatever the model named it. + "id_accessor": to_java_accessor_suffix(id_attr.name), } - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = repository_template.render(**context) - f.write(generated_code) + self.write(f"{class_name}Controller.java", controller_template.render(**context)) diff --git a/besser/generators/spring/spring_entity_generator.py b/besser/generators/spring/spring_entity_generator.py index 7160af645..a6880b1c4 100644 --- a/besser/generators/spring/spring_entity_generator.py +++ b/besser/generators/spring/spring_entity_generator.py @@ -1,286 +1,357 @@ +"""Writes the JPA entity and enumeration classes of a generated Spring Boot project.""" + from collections import defaultdict -import os -import re -from typing import List -from jinja2 import Environment, FileSystemLoader -from besser.BUML.metamodel.structural import DomainModel -from besser.BUML.metamodel.structural.structural import BinaryAssociation, Class, Enumeration, Parameter, Property, StringType, BooleanType, DateTimeType, DateType, FloatType, IntegerType, TimeDeltaType, TimeType -from besser.generators import GeneratorInterface - -class SpringEntityGenerator(GeneratorInterface): - - JAVA_TYPES = { - StringType.name: "String", - BooleanType.name: "Boolean", - IntegerType.name: "Integer", - FloatType.name: "Float", - DateType.name: "LocalDate", - DateTimeType.name: "LocalDateTime", - TimeType.name: "LocalTime", - TimeDeltaType.name: "Duration" - } - - def __init__(self, model: DomainModel, - output_dir: str = "./generated/entity", +from typing import Any, List, Tuple + +from besser.BUML.metamodel.structural import ( + BinaryAssociation, + Class, + DateTimeType, + DateType, + DomainModel, + Enumeration, + Property, + StringType, + TimeDeltaType, + TimeType, +) +from besser.generators.spring._sub_generator import SpringSubGenerator, build_environment +from besser.generators.spring.java_types import ( + java_type_for, + java_type_import, + java_visibility, + is_many, + pluralize, + to_java_accessor_suffix, + to_java_class_name, + to_java_field_name, + to_snake_case, + validate_java_package, +) + +#: An association seen from one of its two sides: (association, source end, target end). +AssociationView = Tuple[BinaryAssociation, Property, Property] + +#: Java types Hibernate can auto-generate an identity value for. +GENERATED_ID_TYPES = ("Integer", "Long") + + +class SpringEntityGenerator(SpringSubGenerator): + """Generates the ``entity`` package of a Spring Boot project (internal helper).""" + + def __init__(self, model: DomainModel, + output_dir: str = "./generated/entity", package_name: str = "com.example.entity"): super().__init__(model, output_dir) - self.package_name: str = package_name - self.enumerations: set[Enumeration] = model.get_enumerations() - self.classes: List[Class] = model.classes_sorted_by_inheritance() - self.relation_owners: dict[str, str] = self._get_relation_owner_map(model) + self.package_name: str = validate_java_package(package_name) + self.association_views: dict[str, List[AssociationView]] = self._build_association_views(model) + self.relation_owners: dict[BinaryAssociation, str] = self._get_relation_owner_map(model) def generate(self): - model: DomainModel = self.model - assoc_map: defaultdict[str, List[BinaryAssociation]] = defaultdict(list) - - for assoc in model.associations: - end1, end2 = list(assoc.ends) - assoc_map[end1.type.name].append(assoc) - assoc_map[end2.type.name].append(assoc) - - for enum in self.enumerations: + for enum in sorted(self.enumerations, key=lambda e: e.name): self._generate_enum_file(enum) - - for cls in self.classes: - self._generate_class_file(cls, assoc_map[cls.name]) - - def _generate_class_file(self, cls: Class, assocs_for_class: List[BinaryAssociation]): - file_path = self.build_generation_path(file_name=f"{cls.name[0].upper() + cls.name[1:]}.java") - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path)) - entity_template = env.get_template("entity.java.j2") - - relations: List[object] = self._prepare_relations(cls, assocs_for_class) - - imports: set[str] = self._get_default_imports().union(self._get_specific_imports_for_class(cls, relations)) - - context = { - "cls": cls, - "package_name": self.package_name, - "imports": sorted(list(imports)), - "is_abstract": cls.is_abstract, - "table_name": self._pluralize(cls.name.lower()), - "parent": cls.parents().pop().name if cls.parents() else None, - "attributes": sorted( - self._prepare_attributes(cls), - key=lambda a: (not a["is_id"], not a["is_enum"], not a["is_list"], a["name"]) - ), - "methods": self._prepare_methods(cls), - "relations": self._prepare_relations(cls, assocs_for_class), - } - - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = entity_template.render(**context) - f.write(generated_code) - - def _prepare_attributes(self, cls: Class) -> List[object]: - attributes: List[object] = [] - - for attr in cls.attributes: - is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) - is_list: bool = attr.multiplicity.max != 1 - is_class: bool = any(attr.type.name == c.name for c in self.classes) - attr_type = None - if is_enum or is_class: - attr_type = attr.type.name - else: - attr_type = self.JAVA_TYPES[attr.type.name] - if is_list: - attr_type = f"List<{attr_type}>" + for cls in self.classes: + self._generate_class_file(cls, self.association_views.get(cls.name, [])) - default_value: str = "" - - if attr.default_value: - if attr.type.name == DateType.name: - default_value = f"""LocalDate.of{attr.default_value["year"], attr.default_value["month"], attr.default_value["day"]}""" - elif attr.type.name == DateTimeType.name: - default_value = f"""LocalDateTime.of{attr.default_value["year"], attr.default_value["month"], attr.default_value["day"], attr.default_value["hour"], attr.default_value["minute"], attr.default_value["second"]}""" - elif attr.type.name == TimeType.name: - default_value = f"""LocalTime.of{attr.default_value["hour"], attr.default_value["minute"], attr.default_value["second"]}""" - elif attr.type.name == TimeDeltaType.name: - default_value = f"Duration.ofSeconds({attr.default_value})" - elif attr.type.name == "str": - default_value = f"\"{attr.default_value}\"" - elif is_enum: - default_value = f"{attr.type.name}.{attr.default_value}" - else: - default_value = attr.default_value + # ------------------------------------------------------------------ + # Associations + # ------------------------------------------------------------------ - attributes.append({ - "is_id": attr.is_id, - "column_name": self._to_snake_case(attr.name), - "is_optional": attr.is_optional, - "is_enum": is_enum, - "is_list": is_list, - "visibility": attr.visibility, - "type": attr_type, - "name": attr.name, - "default_value": default_value - }) - - return attributes - - def _prepare_methods(self, cls: Class) -> List[object]: - methods: List[object] = [] + @staticmethod + def _build_association_views(model: DomainModel) -> dict[str, List[AssociationView]]: + """Index every navigable association side by the class that owns the field. + + Each binary association yields up to two *directed* views. Handling the + two ends separately is what makes self-associations come out right: both + views land in the same class, but with the two distinct role names + instead of the same one twice. + """ + views: defaultdict[str, List[AssociationView]] = defaultdict(list) + + for assoc in sorted(model.associations, key=lambda a: a.name): + ends = sorted(assoc.ends, key=lambda end: (end.name, end.type.name)) + if len(ends) != 2: + continue + first, second = ends + # A field only exists on a side when the opposite end is navigable. + if second.is_navigable: + views[first.type.name].append((assoc, first, second)) + if first.is_navigable: + views[second.type.name].append((assoc, second, first)) - for method in cls.methods: - method_return_type: str = "" + for class_name in views: + views[class_name].sort(key=lambda view: (view[0].name, view[2].name, view[1].name)) - if not method.type: - method_return_type = "void" - else: - is_method_type_enum: bool = any(method.type.name == enum.name for enum in self.enumerations) - is_method_type_class: bool = any(method.type.name == c.name for c in self.classes) + return dict(views) - if is_method_type_enum or is_method_type_class: - method_return_type = method.type.name - else: - method_return_type = self.JAVA_TYPES[method.type.name] + @staticmethod + def _get_relation_owner_map(model: DomainModel) -> dict[BinaryAssociation, str]: + """Pick the owning side of the associations where JPA leaves a choice. + + One-to-one and many-to-many bidirectional associations have no natural + owner, so one has to be elected: without it both sides would emit a + ``@JoinTable``/``@JoinColumn`` and neither a ``mappedBy``. The end whose + role name sorts first wins, which keeps the choice stable across runs and + works for self-associations too (where both ends share a type name but + never a role name). + """ + owners: dict[BinaryAssociation, str] = {} - parameters: List[object] = [] + for assoc in model.associations: + ends = sorted(assoc.ends, key=lambda end: (end.name, end.type.name)) + if len(ends) != 2: + continue + first, second = ends + first_many = is_many(first.multiplicity) + second_many = is_many(second.multiplicity) + ambiguous = (first_many and second_many) or (not first_many and not second_many) + if ambiguous and first.is_navigable and second.is_navigable: + owners[assoc] = first.name - for parameter in method.parameters: - is_enum: bool = any(parameter.type.name == enum.name for enum in self.enumerations) - is_class: bool = any(parameter.type.name == c.name for c in self.classes) - - if is_enum or is_class: - param_type = parameter.type.name - else: - param_type = self.JAVA_TYPES[parameter.type.name] - - parameters.append({ - "name": parameter.name, - "type": param_type - }) - - methods.append({ - "name": method.name, - "visibility": method.visibility, - "return_type": method_return_type, - "code": method.code, - "parameters": parameters - }) + return owners - return methods - - def _prepare_relations(self, cls: Class, assocs_for_class: List[BinaryAssociation]) -> List[object]: - relations: List[object] = [] - - for assoc in assocs_for_class: - ends: List[Property] = list(assoc.ends) - end1, end2 = ends - - if end1.type.name == cls.name and end2.is_navigable: - source: Property = end1 - target: Property = end2 - elif end2.type.name == cls.name and end1.is_navigable: - source: Property = end2 - target: Property = end1 - else: - continue + def _prepare_relations(self, views: List[AssociationView]) -> List[dict]: + relations: List[dict] = [] + for assoc, source, target in views: bidirectional: bool = source.is_navigable and target.is_navigable - source_many = source.multiplicity.max != 1 - target_many = target.multiplicity.max != 1 + source_many: bool = is_many(source.multiplicity) + target_many: bool = is_many(target.multiplicity) - relation: str = None - mapped_by: str = None + relation: str | None = None + mapped_by: str | None = None owning: bool = True + # The foreign key lives in the table of whichever side holds the + # single-valued end, so the column is named after the other one. + join_column: str = f"{to_snake_case(target.type.name)}_id" if not source_many and not target_many: relation = "OneToOne" - if bidirectional and self.relation_owners[assoc.name] != source.type.name: + if bidirectional and self.relation_owners.get(assoc) != source.name: owning = False - mapped_by = source.name + mapped_by = to_java_field_name(source.name) elif not source_many and target_many: relation = "OneToMany" + # A unidirectional @OneToMany puts the foreign key in the target + # table, pointing back at the source. + join_column = f"{to_snake_case(source.type.name)}_id" if bidirectional: owning = False - mapped_by = source.name + mapped_by = to_java_field_name(source.name) elif source_many and not target_many: relation = "ManyToOne" - elif source_many and target_many: + else: relation = "ManyToMany" - if bidirectional and self.relation_owners[assoc.name] != source.type.name: + if bidirectional and self.relation_owners.get(assoc) != source.name: owning = False - mapped_by = source.name - + mapped_by = to_java_field_name(source.name) + + target_type: str = to_java_class_name(target.type.name) + join_table: str = to_snake_case(assoc.name) + source_column: str = f"{to_snake_case(source.type.name)}_id" + target_column: str = f"{to_snake_case(target.type.name)}_id" + + annotations: List[str] = [ + f"@{relation}" + (f'(mappedBy = "{mapped_by}")' if mapped_by else "") + ] + if relation == "ManyToOne" or (owning and relation in ("OneToOne", "OneToMany")): + annotations.append(f'@JoinColumn(name = "{join_column}")') + elif relation == "ManyToMany" and owning: + annotations.append( + f'@JoinTable(name = "{join_table}",\n' + f' joinColumns = @JoinColumn(name = "{source_column}"),\n' + f' inverseJoinColumns = @JoinColumn(name = "{target_column}"))' + ) + relations.append({ - "assoc": assoc.name, - "source_property": source.name, - "target_property": target.name, - "source_cls": source.type.name, - "target_cls": target.type.name, + "assoc": join_table, + "source_property": to_java_field_name(source.name), + "target_property": to_java_field_name(target.name), + "source_column": source_column, + "target_column": target_column, + "annotations": annotations, "relation": relation, "mapped_by": mapped_by, - "join_column": f"{self._to_snake_case(target.type.name)}_id", + "join_column": join_column, "owning": owning, "is_list": target_many, - "type": f"List<{target.type.name}>" if target_many else target.type.name, - "to_snake_case": self._to_snake_case + "type": f"List<{target_type}>" if target_many else target_type, + "accessor": to_java_accessor_suffix(target.name), }) return relations - - def _get_relation_owner_map(self, model: DomainModel) -> dict[str, str]: - ret: dict[str, str] = {} - for assoc in model.associations: - end1, end2 = list(assoc.ends) - if (end1.multiplicity != 1 and end2.multiplicity != 1) or (end1.multiplicity == 1 and end2.multiplicity == 1) and end1.is_navigable and end2.is_navigable: - ret[assoc.name] = end1.type.name + # ------------------------------------------------------------------ + # Classes + # ------------------------------------------------------------------ - return ret + def _generate_class_file(self, cls: Class, views: List[AssociationView]): + class_name: str = to_java_class_name(cls.name) + env = build_environment(trim_blocks=True, lstrip_blocks=True) + entity_template = env.get_template("entity.java.j2") + + relations: List[dict] = self._prepare_relations(views) + attributes: List[dict] = sorted( + self._prepare_attributes(cls), + key=lambda a: (not a["is_id"], not a["is_enum"], not a["is_list"], a["name"]), + ) + imports: set[str] = self._get_mapping_imports(cls, attributes).union( + self._get_specific_imports_for_class(cls, relations) + ) + parents = sorted(cls.parents(), key=lambda parent: parent.name) + + context = { + "class_name": class_name, + "package_name": self.package_name, + "imports": sorted(imports), + "is_abstract": cls.is_abstract, + "table_name": pluralize(to_snake_case(cls.name)), + "parent": to_java_class_name(parents[0].name) if parents else None, + "attributes": attributes, + "methods": self._prepare_methods(cls), + "relations": relations, + } + + self.write(f"{class_name}.java", entity_template.render(**context)) + + def _prepare_attributes(self, cls: Class) -> List[dict]: + attributes: List[dict] = [] + + for attr in cls.attributes: + is_enum: bool = isinstance(attr.type, Enumeration) or any( + attr.type.name == enum.name for enum in self.enumerations + ) + is_list: bool = is_many(attr.multiplicity) + attr_type: str = java_type_for(attr.type, self.model_type_names) + if is_list: + attr_type = f"List<{attr_type}>" + + attributes.append({ + "is_id": attr.is_id, + # Hibernate can only auto-generate identifiers of an integral + # type; a natural key such as an ISBN is supplied by the caller. + "is_generated": attr.is_id and attr_type in GENERATED_ID_TYPES, + "column_name": to_snake_case(attr.name), + "is_optional": attr.is_optional, + "is_enum": is_enum, + "is_list": is_list, + # Rendered as a prefix so that package-private access (which has + # no Java keyword) does not leave a stray space in the declaration. + "visibility": f"{java_visibility(attr.visibility)} ".lstrip(), + "type": attr_type, + "name": to_java_field_name(attr.name), + "accessor": to_java_accessor_suffix(attr.name), + "default_value": self._prepare_default_value(attr, is_enum), + }) + + return attributes + + @staticmethod + def _prepare_default_value(attr: Property, is_enum: bool) -> Any: + if not attr.default_value: + return "" + + type_name = attr.type.name + if type_name == DateType.name: + return (f"LocalDate.of({attr.default_value['year']}, {attr.default_value['month']}, " + f"{attr.default_value['day']})") + if type_name == DateTimeType.name: + return (f"LocalDateTime.of({attr.default_value['year']}, {attr.default_value['month']}, " + f"{attr.default_value['day']}, {attr.default_value['hour']}, " + f"{attr.default_value['minute']}, {attr.default_value['second']})") + if type_name == TimeType.name: + return (f"LocalTime.of({attr.default_value['hour']}, {attr.default_value['minute']}, " + f"{attr.default_value['second']})") + if type_name == TimeDeltaType.name: + return f"Duration.ofSeconds({attr.default_value})" + if type_name == StringType.name: + escaped = str(attr.default_value).replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + if is_enum: + return f"{to_java_class_name(attr.type.name)}.{to_java_field_name(attr.default_value)}" + return attr.default_value + + def _prepare_methods(self, cls: Class) -> List[dict]: + methods: List[dict] = [] + + for method in sorted(cls.methods, key=lambda m: m.name): + parameters = [ + { + "name": to_java_field_name(parameter.name), + "type": java_type_for(parameter.type, self.model_type_names), + } + for parameter in method.parameters + ] + + methods.append({ + "name": to_java_field_name(method.name), + "visibility": java_visibility(method.visibility) or "public", + "return_type": java_type_for(method.type, self.model_type_names) if method.type else "void", + "code": method.code, + "parameters": parameters, + }) + + return methods + + # ------------------------------------------------------------------ + # Enumerations + # ------------------------------------------------------------------ def _generate_enum_file(self, enum: Enumeration): - file_path = self.build_generation_path(file_name=f"{enum.name[0].upper() + enum.name[1:]}.java") - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path)) - entity_template = env.get_template("enum.java.j2") + enum_name: str = to_java_class_name(enum.name) + env = build_environment() + enum_template = env.get_template("enum.java.j2") context = { "package_name": self.package_name, - "name": enum.name[0].upper() + enum.name[1:], - "literals": enum.literals + "name": enum_name, + # ``literals`` is a set: sorting keeps consecutive runs identical. + "literals": sorted( + (to_java_field_name(literal.name) for literal in enum.literals) + ), } - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = entity_template.render(**context) - f.write(generated_code) - - def _get_default_imports(self) -> set[str]: - return set([ - "jakarta.persistence.Entity", - "jakarta.persistence.Table", - "jakarta.persistence.Id", - "jakarta.persistence.GeneratedValue", - "jakarta.persistence.GenerationType", - "jakarta.persistence.Column", - ]) - - def _get_specific_imports_for_class(self, cls: Class, relations: List[object]) -> set[str]: - imports: set[str] = set() + self.write(f"{enum_name}.java", enum_template.render(**context)) + + # ------------------------------------------------------------------ + # Imports + # ------------------------------------------------------------------ + @staticmethod + def _get_mapping_imports(cls: Class, attributes: List[dict]) -> set[str]: + """The ``jakarta.persistence`` imports the class mapping actually uses.""" if cls.is_abstract: - imports.add("jakarta.persistence.MappedSuperclass") + imports = {"jakarta.persistence.MappedSuperclass"} + else: + imports = {"jakarta.persistence.Entity", "jakarta.persistence.Table"} + + if any(attr["is_id"] for attr in attributes): + imports.add("jakarta.persistence.Id") + if any(attr["is_generated"] for attr in attributes): + imports.add("jakarta.persistence.GeneratedValue") + imports.add("jakarta.persistence.GenerationType") + if any(not attr["is_enum"] for attr in attributes): + imports.add("jakarta.persistence.Column") + + return imports + + def _get_specific_imports_for_class(self, cls: Class, relations: List[dict]) -> set[str]: + imports: set[str] = set() - if any(any(attr.type.name == enum.name for enum in self.enumerations) for attr in cls.attributes): + if any(isinstance(attr.type, Enumeration) or + any(attr.type.name == enum.name for enum in self.enumerations) + for attr in cls.attributes): imports.add("jakarta.persistence.Enumerated") imports.add("jakarta.persistence.EnumType") for attr in cls.attributes: - if attr.type.name == DateType.name: - imports.add("java.time.LocalDate") - elif attr.type.name == DateTimeType.name: - imports.add("java.time.LocalDateTime") - elif attr.type.name == TimeType.name: - imports.add("java.time.LocalTime") - elif attr.type.name == TimeDeltaType.name: - imports.add("java.time.Duration") - - if attr.multiplicity.max != 1: + type_import = java_type_import(java_type_for(attr.type, self.model_type_names)) + if type_import: + imports.add(type_import) + + if is_many(attr.multiplicity): imports.add("java.util.List") imports.add("java.util.ArrayList") if attr.default_value: @@ -288,30 +359,18 @@ def _get_specific_imports_for_class(self, cls: Class, relations: List[object]) - for method in cls.methods: if method.type: - if method.type.name == DateType.name: - imports.add("java.time.LocalDate") - elif method.type.name == DateTimeType.name: - imports.add("java.time.LocalDateTime") - elif method.type.name == TimeType.name: - imports.add("java.time.LocalTime") - elif method.type.name == TimeDeltaType.name: - imports.add("java.time.Duration") - + type_import = java_type_import(java_type_for(method.type, self.model_type_names)) + if type_import: + imports.add(type_import) for parameter in method.parameters: - if parameter.type.name == DateType.name: - imports.add("java.time.LocalDate") - elif parameter.type.name == DateTimeType.name: - imports.add("java.time.LocalDateTime") - elif parameter.type.name == TimeType.name: - imports.add("java.time.LocalTime") - elif parameter.type.name == TimeDeltaType.name: - imports.add("java.time.Duration") + type_import = java_type_import(java_type_for(parameter.type, self.model_type_names)) + if type_import: + imports.add(type_import) for relation in relations: if relation["is_list"]: imports.add("java.util.List") imports.add("java.util.ArrayList") - imports.add("java.util.Arrays") if relation["relation"] == "OneToOne": imports.add("jakarta.persistence.OneToOne") if relation["owning"]: @@ -330,17 +389,3 @@ def _get_specific_imports_for_class(self, cls: Class, relations: List[object]) - imports.add("jakarta.persistence.JoinColumn") return imports - - @staticmethod - def _pluralize(name: str) -> str: - if name.endswith("y"): - return name[:-1] + "ies" - elif name.endswith(("s", "x", "z", "ch", "sh")): - return name + "es" - else: - return name + "s" - - @staticmethod - def _to_snake_case(name: str) -> str: - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() diff --git a/besser/generators/spring/spring_http_generator.py b/besser/generators/spring/spring_http_generator.py index 6789b45f4..de41bb369 100644 --- a/besser/generators/spring/spring_http_generator.py +++ b/besser/generators/spring/spring_http_generator.py @@ -1,36 +1,24 @@ -import os -from pathlib import Path -from typing import List +"""Writes the ``.http`` request samples of a generated Spring Boot project.""" -from jinja2 import Environment, FileSystemLoader +from besser.BUML.metamodel.structural import Class, DomainModel +from besser.generators.spring._sub_generator import SpringSubGenerator, build_environment +from besser.generators.spring.java_types import to_java_class_name -from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, StringType, TimeDeltaType, TimeType -from besser.generators.generator_interface import GeneratorInterface -class SpringHttpGenerator(GeneratorInterface): +class SpringHttpGenerator(SpringSubGenerator): + """Generates the ``.http`` scratch files of a Spring Boot project (internal helper).""" - def __init__(self, model: DomainModel, - output_dir: str = "./generated/http"): + def __init__(self, model: DomainModel, output_dir: str = "./generated/http"): super().__init__(model, output_dir) - self.enumerations: set[Enumeration] = model.get_enumerations() - self.classes: set[Class] = model.classes_sorted_by_inheritance() - def generate(self): - for cls in self.classes: - if not cls.is_abstract: - self._generate_http_file(cls) - + for cls in self.concrete_classes(): + self._generate_http_file(cls) + def _generate_http_file(self, cls: Class): - file_path = self.build_generation_path(file_name=f"{cls.name[0].lower() + cls.name[1:]}.http") - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) + class_name: str = to_java_class_name(cls.name) + route: str = class_name[0].lower() + class_name[1:] + env = build_environment(trim_blocks=True, lstrip_blocks=True) http_template = env.get_template("http.http.j2") - context = { - "cls": cls.name - } - - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = http_template.render(**context) - f.write(generated_code) + self.write(f"{route}.http", http_template.render(route=route)) diff --git a/besser/generators/spring/spring_repository_generator.py b/besser/generators/spring/spring_repository_generator.py index e14a10c91..e11c59921 100644 --- a/besser/generators/spring/spring_repository_generator.py +++ b/besser/generators/spring/spring_repository_generator.py @@ -1,105 +1,50 @@ -import os -from pathlib import Path -from typing import List +"""Writes the Spring Data JPA repository interfaces of a generated project.""" -from jinja2 import Environment, FileSystemLoader +from besser.BUML.metamodel.structural import Class, DomainModel +from besser.generators.spring._sub_generator import SpringSubGenerator, build_environment +from besser.generators.spring.java_types import ( + get_id_attribute, + java_type_for, + to_java_class_name, + validate_java_package, +) -from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, Property, StringType, TimeDeltaType, TimeType -from besser.generators.generator_interface import GeneratorInterface -class SpringRepositoryGenerator(GeneratorInterface): +class SpringRepositoryGenerator(SpringSubGenerator): + """Generates the ``repository`` package of a Spring Boot project (internal helper).""" - JAVA_TYPES = { - StringType.name: "String", - BooleanType.name: "Boolean", - IntegerType.name: "Integer", - FloatType.name: "Float", - DateType.name: "LocalDate", - DateTimeType.name: "LocalDateTime", - TimeType.name: "LocalDateTime", - TimeDeltaType.name: "Duration" - } - - def __init__(self, model: DomainModel, + def __init__(self, model: DomainModel, entity_package_name: str, - output_dir: str = "./generated/repository", + output_dir: str = "./generated/repository", package_name: str = "com.example.repository"): super().__init__(model, output_dir) - self.package_name: str = package_name - self.entity_package_name: str = entity_package_name - self.enumerations: set[Enumeration] = model.get_enumerations() - self.classes: set[Class] = model.classes_sorted_by_inheritance() + self.package_name: str = validate_java_package(package_name) + self.entity_package_name: str = validate_java_package(entity_package_name) def generate(self): - for cls in self.classes: - if not cls.is_abstract: - self._generate_repository_file(cls) + for cls in self.concrete_classes(): + self._generate_repository_file(cls) def _generate_repository_file(self, cls: Class): - file_path = self.build_generation_path(file_name=f"I{cls.name[0].upper() + cls.name[1:]}Repository.java") - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) + class_name: str = to_java_class_name(cls.name) + env = build_environment(trim_blocks=True, lstrip_blocks=True) repository_template = env.get_template("irepository.java.j2") - imports: set[str] = set() - imports.add("org.springframework.data.jpa.repository.JpaRepository") - imports.add("org.springframework.stereotype.Repository") - imports.add(f"{self.entity_package_name}.{cls.name}") - - if cls.attributes: - imports.add("java.util.ArrayList") - - methods: List[object] = [] - - for attr in cls.attributes: - is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) - is_list: bool = attr.multiplicity.max != 1 - is_class: bool = any(attr.type.name == c.name for c in self.classes) - - if is_list or attr.is_id: - continue - - method: object = {} - - method["return_value"] = f"ArrayList<{cls.name}>" - method["name"] = f"findAllBy{attr.name[0].upper() + attr.name[1:]}" - - if is_enum or is_class: - parameter_type = attr.type.name - imports.add(f"{self.entity_package_name}.{attr.type.name}") - else: - parameter_type = self.JAVA_TYPES[attr.type.name] - - method["parameter"] = f"{parameter_type} {attr.name}" - - if attr.type.name == DateType.name: - imports.add("java.time.LocalDate") - elif attr.type.name in [DateTimeType.name, TimeType.name]: - imports.add("java.time.LocalDateTime") - elif attr.type.name == TimeDeltaType.name: - imports.add("java.time.Duration") - - if attr.type.name in [DateType.name, DateTimeType.name, TimeType.name]: - methods.append({ - "return_value": f"ArrayList<{cls.name}>", - "name": f"findAllBy{attr.name[0].upper() + attr.name[1:]}Between", - "parameter": f"{parameter_type} start, {parameter_type} end" - }) - - methods.append(method) - - id_attr: Property = next(attr for attr in cls.all_attributes() if attr.is_id) + imports: set[str] = { + "org.springframework.data.jpa.repository.JpaRepository", + "org.springframework.stereotype.Repository", + f"{self.entity_package_name}.{class_name}", + } + methods = self.derived_finder_methods(cls, self.entity_package_name, imports) + id_attr = get_id_attribute(cls) context = { - "package": f"{self.package_name}", + "package": self.package_name, "imports": sorted(imports), - "cls": cls.name, - "methods": sorted(methods, key=lambda m: m["name"]), - "id_type": self.JAVA_TYPES[id_attr.type.name] + "cls": class_name, + "methods": methods, + "id_type": java_type_for(id_attr.type, self.model_type_names), } - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = repository_template.render(**context) - f.write(generated_code) + self.write(f"I{class_name}Repository.java", repository_template.render(**context)) diff --git a/besser/generators/spring/spring_service_generator.py b/besser/generators/spring/spring_service_generator.py index a03af1d3f..555e96092 100644 --- a/besser/generators/spring/spring_service_generator.py +++ b/besser/generators/spring/spring_service_generator.py @@ -1,155 +1,107 @@ -import os +"""Writes the service interfaces and implementations of a generated project.""" + from pathlib import Path from typing import List -from jinja2 import Environment, FileSystemLoader - -from besser.BUML.metamodel.structural.structural import BooleanType, Class, DateTimeType, DateType, DomainModel, Enumeration, FloatType, IntegerType, Property, StringType, TimeDeltaType, TimeType -from besser.generators.generator_interface import GeneratorInterface +from besser.BUML.metamodel.structural import Class, DomainModel +from besser.generators.spring._sub_generator import SpringSubGenerator, build_environment +from besser.generators.spring.java_types import ( + get_id_attribute, + java_type_for, + to_java_class_name, + to_java_field_name, + validate_java_package, +) -class SpringServiceGenerator(GeneratorInterface): - JAVA_TYPES = { - StringType.name: "String", - BooleanType.name: "Boolean", - IntegerType.name: "Integer", - FloatType.name: "Float", - DateType.name: "LocalDate", - DateTimeType.name: "LocalDateTime", - TimeType.name: "LocalDateTime", - TimeDeltaType.name: "Duration" - } +class SpringServiceGenerator(SpringSubGenerator): + """Generates the ``service`` package of a Spring Boot project (internal helper).""" - def __init__(self, model: DomainModel, + def __init__(self, model: DomainModel, entity_package_name: str, repository_package_name: str, - output_dir: str = "./generated/service", + output_dir: str = "./generated/service", package_name: str = "com.example.service"): super().__init__(model, output_dir) - self.package_name: str = package_name - self.entity_package_name: str = entity_package_name - self.repository_package_name: str = repository_package_name - self.enumerations: set[Enumeration] = model.get_enumerations() - self.classes: set[Class] = model.classes_sorted_by_inheritance() + self.package_name: str = validate_java_package(package_name) + self.entity_package_name: str = validate_java_package(entity_package_name) + self.repository_package_name: str = validate_java_package(repository_package_name) def generate(self): - for cls in self.classes: - if not cls.is_abstract: - self._generate_service_files(cls) + for cls in self.concrete_classes(): + self._generate_service_files(cls) def _generate_service_files(self, cls: Class): - templates_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - env = Environment(loader=FileSystemLoader(templates_path), trim_blocks=True) - - file_path = self.build_generation_path(file_name=Path("interfaces") / f"I{cls.name[0].upper() + cls.name[1:]}Service.java") - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - iservice_template = env.get_template("iservice.java.j2") - - imports: set[str] = set() - imports.add(f"{self.entity_package_name}.{cls.name}") - imports.add("java.util.List") - - if cls.attributes: - imports.add("java.util.ArrayList") - imports.add("java.util.Optional") - - methods: List[object] = [] - - for attr in cls.attributes: - is_enum: bool = any(attr.type.name == enum.name for enum in self.enumerations) - is_list: bool = attr.multiplicity.max != 1 - is_class: bool = any(attr.type.name == c.name for c in self.classes) - - if is_list or attr.is_id: - continue - - method: object = {} - - method["return_value"] = f"ArrayList<{cls.name}>" - method["name"] = f"findAllBy{attr.name[0].upper() + attr.name[1:]}" - - if is_enum or is_class: - parameter_type = attr.type.name - imports.add(f"{self.entity_package_name}.{attr.type.name}") - else: - parameter_type = self.JAVA_TYPES[attr.type.name] - - method["parameter"] = f"{parameter_type} {attr.name}" - - if attr.type.name == DateType.name: - imports.add("java.time.LocalDate") - elif attr.type.name in [DateTimeType.name, TimeType.name]: - imports.add("java.time.LocalDateTime") - elif attr.type.name == TimeDeltaType.name: - imports.add("java.time.Duration") - - if attr.type.name in [DateType.name, DateTimeType.name, TimeType.name]: - methods.append({ - "return_value": f"ArrayList<{cls.name}>", - "name": f"findAllBy{attr.name[0].upper() + attr.name[1:]}Between", - "parameter": f"{parameter_type} start, {parameter_type} end" - }) + class_name: str = to_java_class_name(cls.name) + env = build_environment(trim_blocks=True, lstrip_blocks=True) - methods.append(method) - - methods.extend(self._get_crud_methods(cls)) + imports: set[str] = { + f"{self.entity_package_name}.{class_name}", + "java.util.List", + "java.util.Optional", + } + # The very same list the repository renders, so that the implementation + # below can delegate to it method for method. + methods: List[dict] = self.derived_finder_methods(cls, self.entity_package_name, imports) + methods.extend(self._get_crud_methods(cls, class_name)) + methods.sort(key=lambda method: method["name"]) context = { "package": f"{self.package_name}.interfaces", "imports": sorted(imports), - "cls": cls.name, - "methods": sorted(methods, key=lambda m: m["name"]) + "cls": class_name, + "instance": to_java_field_name(class_name[0].lower() + class_name[1:]), + "methods": methods, } - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = iservice_template.render(**context) - f.write(generated_code) - - file_path = self.build_generation_path(file_name=Path("impl") / f"{cls.name[0].upper() + cls.name[1:]}Service.java") - Path(file_path).parent.mkdir(parents=True, exist_ok=True) - service_template = env.get_template("service.java.j2") - - context["package"] = f"{self.package_name}.impl" + self.write( + Path("interfaces") / f"I{class_name}Service.java", + env.get_template("iservice.java.j2").render(**context), + ) imports.add("org.springframework.beans.factory.annotation.Autowired") imports.add("org.springframework.stereotype.Service") - imports.add(f"{self.package_name}.interfaces.I{cls.name[0].upper() + cls.name[1:]}Service") - imports.add(f"{self.repository_package_name}.I{cls.name[0].upper() + cls.name[1:]}Repository") - - context["imports"] = sorted(imports) + imports.add(f"{self.package_name}.interfaces.I{class_name}Service") + imports.add(f"{self.repository_package_name}.I{class_name}Repository") - for method in context["methods"]: + for method in methods: parameter: str = method["parameter"] tokens: List[str] = parameter.split(", ") method["parameter_names"] = ", ".join(token.split(" ")[1] for token in tokens) if parameter else "" - with open(file_path, mode="w", encoding="utf-8") as f: - generated_code = service_template.render(**context) - f.write(generated_code) + context["package"] = f"{self.package_name}.impl" + context["imports"] = sorted(imports) + + self.write( + Path("impl") / f"{class_name}Service.java", + env.get_template("service.java.j2").render(**context), + ) + + def _get_crud_methods(self, cls: Class, class_name: str) -> List[dict]: + id_attr = get_id_attribute(cls) + instance_name: str = to_java_field_name(class_name[0].lower() + class_name[1:]) - def _get_crud_methods(self, cls: Class) -> List[object]: - id_attr: Property = next(attr for attr in cls.all_attributes() if attr.is_id) - return [ { - "return_value": f"List<{cls.name}>", + "return_value": f"List<{class_name}>", "name": "findAll", - "parameter": "" + "parameter": "", }, { - "return_value": f"Optional<{cls.name}>", + "return_value": f"Optional<{class_name}>", "name": "findById", - "parameter": f"{self.JAVA_TYPES[id_attr.type.name]} {id_attr.name}" + "parameter": (f"{java_type_for(id_attr.type, self.model_type_names)} " + f"{to_java_field_name(id_attr.name)}"), }, { - "return_value": f"{cls.name}", + "return_value": class_name, "name": "save", - "parameter": f"{cls.name} {cls.name[0].lower() + cls.name[1:]}" + "parameter": f"{class_name} {instance_name}", }, { "return_value": "void", "name": "delete", - "parameter": f"{cls.name} {cls.name[0].lower() + cls.name[1:]}" - } + "parameter": f"{class_name} {instance_name}", + }, ] diff --git a/besser/generators/spring/templates/controller.java.j2 b/besser/generators/spring/templates/controller.java.j2 index 3dbc87c54..49857d9dc 100644 --- a/besser/generators/spring/templates/controller.java.j2 +++ b/besser/generators/spring/templates/controller.java.j2 @@ -5,39 +5,39 @@ import {{ import }}; {% endfor %} @RestController -@RequestMapping("/api/{{ cls[0] | lower + cls[1:] }}") +@RequestMapping("/api/{{ route }}") public class {{ cls }}Controller { @Autowired - private I{{ cls }}Service _{{ cls[0] | lower + cls[1:] }}Service; + private I{{ cls }}Service _{{ instance }}Service; @GetMapping public ResponseEntity> getAll() { - return ResponseEntity.ok(_{{ cls[0] | lower + cls[1:] }}Service.findAll()); + return ResponseEntity.ok(_{{ instance }}Service.findAll()); } @GetMapping("/{id}") public ResponseEntity<{{ cls }}> getById(@PathVariable {{ id_type }} id) { - return _{{ cls[0] | lower + cls[1:] }}Service.findById(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + return _{{ instance }}Service.findById(id).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<{{ cls }}> create(@RequestBody {{ cls }} entity) { - {{ cls }} saved = _{{ cls[0] | lower + cls[1:] }}Service.save(entity); + {{ cls }} saved = _{{ instance }}Service.save(entity); return ResponseEntity.ok(saved); } @PutMapping("/{id}") public ResponseEntity<{{ cls }}> update(@PathVariable {{ id_type }} id, @RequestBody {{ cls }} entity) { - Optional<{{ cls }}> existing = _{{ cls[0] | lower + cls[1:] }}Service.findById(id); + Optional<{{ cls }}> existing = _{{ instance }}Service.findById(id); if (existing.isEmpty()) { return ResponseEntity.notFound().build(); } - entity.setId(id); - {{ cls }} updated = _{{ cls[0] | lower + cls[1:] }}Service.save(entity); + entity.set{{ id_accessor }}(id); + {{ cls }} updated = _{{ instance }}Service.save(entity); return ResponseEntity.ok(updated); } @@ -45,15 +45,15 @@ public class {{ cls }}Controller { @DeleteMapping("/{id}") public ResponseEntity deleteById(@PathVariable {{ id_type }} id) { - Optional<{{ cls }}> existing = _{{ cls[0] | lower + cls[1:] }}Service.findById(id); + Optional<{{ cls }}> existing = _{{ instance }}Service.findById(id); if (existing.isEmpty()) { return ResponseEntity.notFound().build(); } - _{{ cls[0] | lower + cls[1:] }}Service.delete(existing.get()); + _{{ instance }}Service.delete(existing.get()); return ResponseEntity.noContent().build(); } -} \ No newline at end of file +} diff --git a/besser/generators/spring/templates/entity.java.j2 b/besser/generators/spring/templates/entity.java.j2 index 2b59166e6..07e70532a 100644 --- a/besser/generators/spring/templates/entity.java.j2 +++ b/besser/generators/spring/templates/entity.java.j2 @@ -1,101 +1,83 @@ package {{ package_name }}; -{% for import in imports -%} +{% for import in imports %} import {{ import }}; -{% endfor -%} +{% endfor %} {% if is_abstract %} @MappedSuperclass {% else %} @Entity @Table(name = "{{ table_name }}") -{% endif -%} -public{% if is_abstract %} abstract{% endif %} class {{ cls.name }}{% if parent %} extends {{parent}}{% endif %} { - {% for attribute in attributes -%} - {% if attribute.is_id %} +{% endif %} +public {% if is_abstract %}abstract {% endif %}class {{ class_name }}{% if parent %} extends {{ parent }}{% endif %} { + +{% for attribute in attributes %} +{% if attribute.is_id %} @Id +{% endif %} +{% if attribute.is_generated %} @GeneratedValue(strategy = GenerationType.IDENTITY) - {%- endif %} - {% if attribute.is_enum -%} +{% endif %} +{% if attribute.is_enum %} @Enumerated(EnumType.STRING) - {% else -%} +{% else %} @Column(name = "{{ attribute.column_name }}"{% if not attribute.is_optional %}, nullable = false{% endif %}) - {% endif -%} - {% if not attribute.is_list %} - {{- attribute.visibility }} {{ attribute.type }} {{ attribute.name }}{% if attribute.default_value %} = {{ attribute.default_value }}{% endif %}; - {%- else -%} - {{- attribute.visibility }} {{ attribute.type }} {{ attribute.name }} = new ArrayList<>({% if attribute.default_value %}Arrays.asList({{ attribute.default_value }}){% endif %}); - {%- endif %} - {% endfor -%} - - {% for rel in relations -%} - - {% if rel.relation == "OneToOne" %} - @OneToOne{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} - {% if rel.owning -%} - @JoinColumn(name = "{{ rel.join_column }}") - {%- endif %} - {% endif -%} - - {% if rel.relation == "OneToMany" %} - @OneToMany{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} - {% if rel.owning -%} - @JoinColumn(name = "{{ rel.join_column }}") - {%- endif %} - {% endif -%} - - {% if rel.relation == "ManyToOne" %} - @ManyToOne - @JoinColumn(name = "{{ rel.join_column }}") - {% endif -%} - - {% if rel.relation == "ManyToMany" %} - @ManyToMany{% if rel.mapped_by %}(mappedBy = "{{ rel.mapped_by }}"){% endif %} - {% if rel.owning -%} - @JoinTable(name = "{{ rel.to_snake_case(rel.assoc) }}", - joinColumns = @JoinColumn(name = "{{ rel.to_snake_case(rel.source_cls) }}_id"), - inverseJoinColumns = @JoinColumn(name = "{{ rel.to_snake_case(rel.target_cls) }}_id")) - {%- endif %} - {% endif -%} +{% endif %} +{% if attribute.is_list %} + {{ attribute.visibility }}{{ attribute.type }} {{ attribute.name }} = new ArrayList<>({% if attribute.default_value %}Arrays.asList({{ attribute.default_value }}){% endif %}); +{% else %} + {{ attribute.visibility }}{{ attribute.type }} {{ attribute.name }}{% if attribute.default_value %} = {{ attribute.default_value }}{% endif %}; +{% endif %} - {%- if rel.is_list -%} - private {{ rel.type }} {{ rel.target_property }} = new ArrayList<>(); - {% else -%} - private {{ rel.type }} {{ rel.target_property }}; - {% endif -%} - {%- endfor -%} +{% endfor %} +{% for rel in relations %} +{% for annotation in rel.annotations %} + {{ annotation }} +{% endfor %} + private {{ rel.type }} {{ rel.target_property }}{% if rel.is_list %} = new ArrayList<>(){% endif %}; - {% if not is_abstract %} - public {{ cls.name }}() { } +{% endfor %} +{% if not is_abstract %} + public {{ class_name }}() { } - {% if attributes -%} - public {{ cls.name }}( - {%- for attribute in attributes -%} - {{ attribute.type }} {{ attribute.name }}{% if not loop.last %}, {% endif %} - {%- endfor %}) { - {%- for attribute in attributes %} +{% if attributes %} + public {{ class_name }}({% for attribute in attributes %}{{ attribute.type }} {{ attribute.name }}{% if not loop.last %}, {% endif %}{% endfor %}) { +{% for attribute in attributes %} this.{{ attribute.name }} = {{ attribute.name }}; - {%- endfor %} +{% endfor %} } - {%- endif %} - {% endif -%} - {% for method in methods %} - {{ method.visibility }} {{ method.return_type }} {{ method.name }}( - {%- for parameter in method.parameters -%} - {{ parameter.type }} {{ parameter.name }}{% if not loop.last %}, {% endif %} - {%- endfor %}) { - {% if method.code %}{{ method.code }}{% else %}{% if method.return_type != "void" %}return null;{% endif %}{% endif %} +{% endif %} +{% endif %} +{% for method in methods %} + {{ method.visibility }} {{ method.return_type }} {{ method.name }}({% for parameter in method.parameters %}{{ parameter.type }} {{ parameter.name }}{% if not loop.last %}, {% endif %}{% endfor %}) { +{% if method.code %} + {{ method.code }} +{% elif method.return_type != "void" %} + return null; +{% endif %} } - {% endfor -%} - {% for attribute in attributes %} - public {{ attribute.type }} get{{ attribute.name[0] | upper + attribute.name[1:] }}() { +{% endfor %} +{% for attribute in attributes %} + public {{ attribute.type }} get{{ attribute.accessor }}() { return this.{{ attribute.name }}; } - public void set{{ attribute.name[0] | upper + attribute.name[1:] }}({{ attribute.type }} {{ attribute.name }}) { + public void set{{ attribute.accessor }}({{ attribute.type }} {{ attribute.name }}) { this.{{ attribute.name }} = {{ attribute.name }}; } - {% endfor %} -} \ No newline at end of file + +{% endfor %} +{% for rel in relations %} + public {{ rel.type }} get{{ rel.accessor }}() { + return this.{{ rel.target_property }}; + } + + public void set{{ rel.accessor }}({{ rel.type }} {{ rel.target_property }}) { + this.{{ rel.target_property }} = {{ rel.target_property }}; + } + +{% endfor %} +} diff --git a/besser/generators/spring/templates/enum.java.j2 b/besser/generators/spring/templates/enum.java.j2 index 7d217911a..0fdb6d071 100644 --- a/besser/generators/spring/templates/enum.java.j2 +++ b/besser/generators/spring/templates/enum.java.j2 @@ -1,5 +1,5 @@ package {{ package_name }}; public enum {{ name }} { - {% for lit in literals %}{{ lit.name }}{% if not loop.last %}, {% endif %}{% endfor %} -} \ No newline at end of file + {{ literals | join(", ") }} +} diff --git a/besser/generators/spring/templates/http.http.j2 b/besser/generators/spring/templates/http.http.j2 index ce3a8750c..9c38f0a88 100644 --- a/besser/generators/spring/templates/http.http.j2 +++ b/besser/generators/spring/templates/http.http.j2 @@ -1,27 +1,27 @@ ### GET ALL -GET http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }} +GET http://localhost:8080/api/{{ route }} Accept: application/json ### GET BY ID -GET http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }}/ +GET http://localhost:8080/api/{{ route }}/ Accept: application/json ### CREATE -POST http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }} +POST http://localhost:8080/api/{{ route }} Content-Type: application/json { - + } ### UPDATE -PUT http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }}/ +PUT http://localhost:8080/api/{{ route }}/ Content-Type: application/json { - + } ### DELETE -DELETE http://localhost:8080/api/{{ cls[0] | lower + cls[1:] }}/ -Accept: application/json \ No newline at end of file +DELETE http://localhost:8080/api/{{ route }}/ +Accept: application/json diff --git a/besser/generators/spring/templates/irepository.java.j2 b/besser/generators/spring/templates/irepository.java.j2 index 400e2ebfa..b37325249 100644 --- a/besser/generators/spring/templates/irepository.java.j2 +++ b/besser/generators/spring/templates/irepository.java.j2 @@ -7,8 +7,8 @@ import {{ import }}; @Repository public interface I{{ cls }}Repository extends JpaRepository<{{ cls }}, {{ id_type }}> { - {% for method in methods %} +{% for method in methods %} {{ method.return_value }} {{ method.name }}({{ method.parameter }}); - {% endfor %} - -} \ No newline at end of file +{% endfor %} + +} diff --git a/besser/generators/spring/templates/iservice.java.j2 b/besser/generators/spring/templates/iservice.java.j2 index 05e2c1f0c..6ea422bc1 100644 --- a/besser/generators/spring/templates/iservice.java.j2 +++ b/besser/generators/spring/templates/iservice.java.j2 @@ -6,8 +6,8 @@ import {{ import }}; public interface I{{ cls }}Service { - {% for method in methods %} +{% for method in methods %} {{ method.return_value }} {{ method.name }}({{ method.parameter }}); - {% endfor %} - -} \ No newline at end of file +{% endfor %} + +} diff --git a/besser/generators/spring/templates/main.java.j2 b/besser/generators/spring/templates/main.java.j2 index ae2970faa..afefe1313 100644 --- a/besser/generators/spring/templates/main.java.j2 +++ b/besser/generators/spring/templates/main.java.j2 @@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class {{ name[0] | upper + name[1:] }} { +public class {{ name }} { public static void main(String[] args) { - SpringApplication.run({{ name[0] | upper + name[1:] }}.class, args); + SpringApplication.run({{ name }}.class, args); } -} \ No newline at end of file +} diff --git a/besser/generators/spring/templates/service.java.j2 b/besser/generators/spring/templates/service.java.j2 index 7b44a32d0..5380af3d4 100644 --- a/besser/generators/spring/templates/service.java.j2 +++ b/besser/generators/spring/templates/service.java.j2 @@ -8,14 +8,13 @@ import {{ import }}; public class {{ cls }}Service implements I{{ cls }}Service { @Autowired - private I{{ cls }}Repository _{{ cls[0] | lower + cls[1:] }}Repository; + private I{{ cls }}Repository _{{ instance }}Repository; - {% for method in methods %} -@Override +{% for method in methods %} + @Override public {{ method.return_value }} {{ method.name }}({{ method.parameter }}) { - {% if method.return_value != "void" %}return {% endif %}_{{ cls[0] | lower + cls[1:] }}Repository.{{ method.name }}({{ method.parameter_names }}); + {{ "return " if method.return_value != "void" else "" }}_{{ instance }}Repository.{{ method.name }}({{ method.parameter_names }}); } - - {% endfor %} -} \ No newline at end of file +{% endfor %} +} diff --git a/besser/generators/spring/templates/test.java.j2 b/besser/generators/spring/templates/test.java.j2 index 959884ff1..316cf1fd9 100644 --- a/besser/generators/spring/templates/test.java.j2 +++ b/besser/generators/spring/templates/test.java.j2 @@ -4,10 +4,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class {{ name[0] | upper + name[1:] }}Tests { +class {{ name }}Tests { @Test void contextLoads() { } -} \ No newline at end of file +} diff --git a/besser/utilities/web_modeling_editor/backend/constants/constants.py b/besser/utilities/web_modeling_editor/backend/constants/constants.py index 6b95b4ff5..eded7cdf5 100644 --- a/besser/utilities/web_modeling_editor/backend/constants/constants.py +++ b/besser/utilities/web_modeling_editor/backend/constants/constants.py @@ -1,3 +1,10 @@ +from besser.generators.spring.spring_backend_generator import ( + DEFAULT_JAVA_VERSION as _SPRING_DEFAULT_JAVA_VERSION, + DEFAULT_SPRING_APP_NAME as _SPRING_DEFAULT_APP_NAME, + DEFAULT_SPRING_BOOT_VERSION as _SPRING_DEFAULT_BOOT_VERSION, + DEFAULT_SPRING_PACKAGE_NAME as _SPRING_DEFAULT_PACKAGE_NAME, +) + # API Configuration API_VERSION = "1.0.0" @@ -20,6 +27,14 @@ DEFAULT_DJANGO_PROJECT_NAME = "myproject" DEFAULT_DJANGO_APP_NAME = "myapp" DEFAULT_SUPABASE_USER_ROOT = "User" +# Owned by the generator (generators must not depend on the web backend), and +# re-exported here so the API layer has a single place to read them from. +DEFAULT_SPRING_BOOT_VERSION = _SPRING_DEFAULT_BOOT_VERSION +DEFAULT_JAVA_VERSION = _SPRING_DEFAULT_JAVA_VERSION +DEFAULT_SPRING_APP_NAME = _SPRING_DEFAULT_APP_NAME +DEFAULT_SPRING_PACKAGE_NAME = _SPRING_DEFAULT_PACKAGE_NAME +# Only the web editor wraps the generated sources in a named project folder. +DEFAULT_SPRING_PROJECT_NAME = "springproject" # CORS defaults DEFAULT_CORS_ORIGINS = [ diff --git a/besser/utilities/web_modeling_editor/backend/routers/generation_router.py b/besser/utilities/web_modeling_editor/backend/routers/generation_router.py index 6033616f4..429c1ad5e 100644 --- a/besser/utilities/web_modeling_editor/backend/routers/generation_router.py +++ b/besser/utilities/web_modeling_editor/backend/routers/generation_router.py @@ -104,6 +104,11 @@ DEFAULT_DJANGO_PROJECT_NAME, DEFAULT_DJANGO_APP_NAME, DEFAULT_SUPABASE_USER_ROOT, + DEFAULT_SPRING_BOOT_VERSION, + DEFAULT_JAVA_VERSION, + DEFAULT_SPRING_APP_NAME, + DEFAULT_SPRING_PACKAGE_NAME, + DEFAULT_SPRING_PROJECT_NAME, ) # Centralized error handling @@ -770,6 +775,8 @@ async def _handle_class_diagram_generation( # Generate based on generator type if generator_type == "django": return await _generate_django(buml_model, generator_class, config, temp_dir) + if generator_type == "spring": + return await _generate_spring(buml_model, generator_class, config, temp_dir) if generator_type == "sql": return await _generate_sql(buml_model, generator_class, config, temp_dir) if generator_type == "supabase": @@ -937,6 +944,59 @@ def _run_generate_in_temp_dir(): ) +async def _generate_spring(buml_model, generator_class, config: dict, temp_dir: str): + """Generate a Spring Boot project and return it as a ZIP.""" + config = config or {} + project_name = config.get("project_name") or DEFAULT_SPRING_PROJECT_NAME + app_name = config.get("app_name") or DEFAULT_SPRING_APP_NAME + spring_boot_version = config.get("spring_boot_version") or DEFAULT_SPRING_BOOT_VERSION + java_version = config.get("java_version") or DEFAULT_JAVA_VERSION + package_name = config.get("package_name") or DEFAULT_SPRING_PACKAGE_NAME + + # Sanitize project_name to prevent path traversal + project_name = os.path.basename(project_name) + if not project_name: + project_name = DEFAULT_SPRING_PROJECT_NAME + + project_dir = _safe_path(temp_dir, project_name) + os.makedirs(project_dir, exist_ok=True) + + # An invalid package name is a user input error, not a server fault. + try: + generator_instance = generator_class( + buml_model, + output_dir=project_dir, + app_name=app_name, + spring_boot_version=spring_boot_version, + java_version=java_version, + package_name=package_name, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + await asyncio.to_thread(generator_instance.generate) + + if not os.listdir(project_dir): + raise ValueError("Spring Boot project generation failed: Output directory is empty") + + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: + for root, _, files in os.walk(project_dir): + for file in files: + file_path = os.path.join(root, file) + arc_name = os.path.relpath(file_path, project_dir) + zip_file.write(file_path, arc_name) + + zip_buffer.seek(0) + file_name = get_filename_for_generator("spring") + + return StreamingResponse( + zip_buffer, + media_type="application/zip", + headers={"Content-Disposition": f'attachment; filename="{file_name}"'}, + ) + + async def _generate_sql(buml_model, generator_class, config: dict, temp_dir: str): """Generate SQL files.""" dialect = DEFAULT_SQL_DIALECT diff --git a/docs/source/generators.rst b/docs/source/generators.rst index a23c111a6..dcc7f16d4 100644 --- a/docs/source/generators.rst +++ b/docs/source/generators.rst @@ -32,6 +32,10 @@ Choosing a Generator - Structural - ZIP (FastAPI + SQLAlchemy) - You need only a REST API backend without frontend + * - **Spring Boot** + - Structural + - ZIP (Maven project) + - You need a Java REST backend with JPA entities, repositories and controllers * - **REST API** - Structural - Python files @@ -133,6 +137,7 @@ Generate code for various frameworks and programming languages: generators/django generators/backend + generators/spring generators/rest_api generators/python generators/pydantic diff --git a/docs/source/generators/spring.rst b/docs/source/generators/spring.rst new file mode 100644 index 000000000..60b5c0ff7 --- /dev/null +++ b/docs/source/generators/spring.rst @@ -0,0 +1,159 @@ +Spring Boot Generator +===================== + +BESSER provides a code generator for `Spring Boot `_ +backends. From a structural model it produces a complete, runnable Maven project: +JPA entities, Spring Data repositories, a service layer, REST controllers, and a +Maven wrapper, so the generated project builds and starts without any further setup. + +**B-UML Model required** + +- :doc:`../buml_language/model_types/structural`: the classes, attributes, + associations and enumerations that become the persistence and API layers. + +**Getting started** + +You can either use the :doc:`../web_editor` to draw the structural model and generate +the code directly, or use the BESSER Python API as shown below. This example uses the +:doc:`../examples/library_example` as input. + +.. code-block:: python + + from besser.generators.spring import SpringBackendGenerator + + generator: SpringBackendGenerator = SpringBackendGenerator( + model=library_model, + output_dir="output/library", + app_name="LibraryApplication", + package_name="com.example.library", + ) + generator.generate() + +**Configuration Parameters** + +Only ``model`` is required. Every other parameter is keyword-only and has a default, +so the shortest useful call is ``SpringBackendGenerator(model, output_dir="...")``. + +- ``model``: the structural model the backend is generated from. +- ``output_dir``: the directory the project is written to. When omitted, the + generator writes to ``/output``. +- ``spring_boot_version``: the ``spring-boot-starter-parent`` version written to + the POM. Defaults to ``"3.4.4"``. +- ``java_version``: the ``java.version`` property of the generated POM. Defaults + to ``"21"``. +- ``app_name``: the application name, which is also the name of the generated main + class and of its test class. Defaults to ``"Application"``. +- ``package_name``: the root Java package of the generated sources. It is validated + as a legal Java package name and determines the directory nesting under + ``src/main/java``. Defaults to ``"com.example"``. +- ``group_id``: the Maven ``groupId`` of the generated project. Defaults to + ``"com.example"``. +- ``description``: the Maven ``description`` of the generated project. Defaults to + an empty string. + +What is Generated +----------------- + +- **Entities**: each class becomes a JPA ``@Entity`` with an ``@Table``, getters and + setters, and an all-args constructor. An attribute marked as the model's identifier + becomes ``@Id``; it additionally gets ``@GeneratedValue(strategy = GenerationType.IDENTITY)`` + only when its type maps to a Java ``Integer`` or ``Long``, since Hibernate cannot + auto-generate a ``String`` identifier. An abstract class becomes a ``@MappedSuperclass`` + instead, and generalizations are emitted as Java ``extends``. +- **Enumerations**: each enumeration becomes a Java ``enum``, and attributes typed by + it are annotated ``@Enumerated(EnumType.STRING)``. +- **Associations**: the multiplicity of each end decides the JPA annotation — + ``@OneToOne``, ``@OneToMany``, ``@ManyToOne`` or ``@ManyToMany`` — with + ``@JoinColumn`` on the owning side, ``@JoinTable`` for many-to-many, and + ``mappedBy`` on the inverse side. A non-navigable end produces no field. +- **Repositories**: one ``IRepository extends JpaRepository`` per concrete + class, with a derived finder for each simple attribute, plus a ``...Between`` + finder for date and time attributes. +- **Services**: an ``IService`` interface and a ``Service`` + implementation per concrete class, providing CRUD over the repository. +- **Controllers**: a ``@RestController`` per concrete class, mapped at + ``/api/``, exposing ``GET``, ``GET /{id}``, ``POST``, ``PUT /{id}`` and + ``DELETE /{id}``. +- **HTTP request files**: a ``.http`` file per concrete class under + ``src/test/resources/http``, ready to run against the started application from an + IDE or REST client. + +Abstract classes take part in the entity hierarchy but get no repository, service, +controller or ``.http`` file of their own. + +Output +------ + +For a model with an ``Author`` class, a ``Book`` class and a ``Genre`` enumeration, +generated with ``app_name="LibraryApplication"`` and +``package_name="com.example.library"``: + +.. code-block:: text + + output/library/ + ├── pom.xml + ├── mvnw + ├── mvnw.cmd + ├── .mvn/ + │ └── wrapper/ + │ └── maven-wrapper.properties + └── src/ + ├── main/ + │ ├── java/com/example/library/ + │ │ ├── LibraryApplication.java + │ │ ├── entity/ + │ │ │ ├── Author.java + │ │ │ ├── Book.java + │ │ │ └── Genre.java + │ │ ├── repository/ + │ │ │ ├── IAuthorRepository.java + │ │ │ └── IBookRepository.java + │ │ ├── service/ + │ │ │ ├── interfaces/ + │ │ │ │ ├── IAuthorService.java + │ │ │ │ └── IBookService.java + │ │ │ └── impl/ + │ │ │ ├── AuthorService.java + │ │ │ └── BookService.java + │ │ └── controller/ + │ │ ├── AuthorController.java + │ │ └── BookController.java + │ └── resources/ + │ └── application.properties + └── test/ + ├── java/com/example/library/ + │ └── LibraryApplicationTests.java + └── resources/http/ + ├── author.http + └── book.http + +How to Run the Application +-------------------------- + +**Requirement**: a JDK matching the ``java_version`` the project was generated with +(21 by default). Maven itself does not need to be installed — the generated project +carries a Maven wrapper. + +Go to the project folder and run: + +.. code-block:: bash + + # Start the application (use mvnw.cmd on Windows) + ./mvnw spring-boot:run + +The REST API is then served at `http://localhost:8080 `_, with +each class exposed under ``/api/`` — for example +`http://localhost:8080/api/book `_. + +The generated project is configured for an in-memory +`H2 database `_ with ``spring.jpa.hibernate.ddl-auto=update``, +so the schema is created at startup and no database has to be provisioned to try the +application out. The data is discarded when the application stops. The H2 console is +enabled at `http://localhost:8080/h2-console `_. +Point ``spring.datasource.*`` in ``src/main/resources/application.properties`` at a real +database to persist beyond a run. + +.. note:: + + The :doc:`../web_editor` offers this generator in the **Web** group of the + **Generate** menu, and returns the project as a ZIP archive. diff --git a/setup.cfg b/setup.cfg index 7b75c10ca..8112243f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,9 @@ packages=find: [options.package_data] * = *.j2 +; Static (non-templated) files shipped with a generator, e.g. the Maven wrapper +; the Spring generator copies into the projects it generates. +besser.generators.spring = resources/* [options.extras_require] nn = diff --git a/tests/generators/spring/test_spring_generator.py b/tests/generators/spring/test_spring_generator.py new file mode 100644 index 000000000..be19d7d66 --- /dev/null +++ b/tests/generators/spring/test_spring_generator.py @@ -0,0 +1,337 @@ +"""Tests for the Spring Boot backend generator. + +The models come from the centralized fixtures in ``tests/conftest.py``; the +Spring-specific fixtures below only add what a JPA mapping additionally needs +(identifiers, a ``time`` attribute and an inheritance pair). +""" + +import copy +import filecmp +import os +from pathlib import Path + +import pytest + +from besser.BUML.metamodel.structural import ( + Class, + Generalization, + IntegerType, + Property, + StringType, + TimeType, +) +from besser.generators.spring import SpringBackendGenerator +from besser.generators.spring.java_types import ( + JAVA_TYPES, + to_java_identifier, + validate_java_package, +) + +PACKAGE_DIR = Path("src", "main", "java", "com", "example") + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def spring_library_model(library_book_author_model): + """``library_book_author_model`` completed with what a JPA mapping needs. + + * every concrete class gets an identifier (JPA entities require one), + * ``Library`` gets a ``time`` attribute (to pin the ``TimeType`` mapping), + * ``Author`` gets an abstract parent (to exercise ``@MappedSuperclass``). + """ + model = library_book_author_model + + library = model.get_class_by_name("Library") + library.add_attribute(Property(name="id", type=IntegerType, is_id=True)) + library.add_attribute(Property(name="openingTime", type=TimeType)) + + book = model.get_class_by_name("Book") + book.add_attribute(Property(name="isbn", type=StringType, is_id=True)) + + person = Class(name="Person", is_abstract=True) + person.add_attribute(Property(name="id", type=IntegerType, is_id=True)) + model.add_type(person) + model.add_generalization( + Generalization(general=person, specific=model.get_class_by_name("Author")) + ) + + return model + + +@pytest.fixture +def generated_library(spring_library_model, tmp_path): + """Path: A generated Spring Boot project for the library model.""" + output_dir = tmp_path / "project" + SpringBackendGenerator(spring_library_model, output_dir=str(output_dir), + app_name="Library").generate() + return output_dir + + +def read(project_dir: Path, *parts: str) -> str: + return (project_dir.joinpath(*parts)).read_text(encoding="utf-8") + + +# --------------------------------------------------------------------------- +# Construction +# --------------------------------------------------------------------------- + +def test_generator_is_constructible_like_every_other_generator(spring_library_model, tmp_path): + """The web editor instantiates generators as ``(model, output_dir=...)``.""" + generator = SpringBackendGenerator(spring_library_model, output_dir=str(tmp_path)) + generator.generate() + + assert (tmp_path / "pom.xml").exists() + + +def test_generator_is_registered_and_buildable_by_the_backend(spring_library_model, tmp_path): + """The registry entry has to be instantiable exactly as ``_generate_standard`` does.""" + from besser.utilities.web_modeling_editor.backend.config.generators import SUPPORTED_GENERATORS + + generator_info = SUPPORTED_GENERATORS["spring"] + generator = generator_info.generator_class(spring_library_model, output_dir=str(tmp_path)) + generator.generate() + + assert generator_info.output_type == "zip" + assert (tmp_path / "pom.xml").exists() + + +def test_defaults_are_shared_with_the_backend_constants(): + """The backend constants must mirror the generator-owned defaults.""" + from besser.generators.spring import ( + DEFAULT_JAVA_VERSION, + DEFAULT_SPRING_APP_NAME, + DEFAULT_SPRING_BOOT_VERSION, + ) + from besser.utilities.web_modeling_editor.backend.constants import constants + + assert constants.DEFAULT_SPRING_BOOT_VERSION == DEFAULT_SPRING_BOOT_VERSION + assert constants.DEFAULT_JAVA_VERSION == DEFAULT_JAVA_VERSION + assert constants.DEFAULT_SPRING_APP_NAME == DEFAULT_SPRING_APP_NAME + + +def test_relative_output_dir_is_not_joined_twice(spring_library_model, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + SpringBackendGenerator(spring_library_model, output_dir="generated/myapp").generate() + + assert (tmp_path / "generated" / "myapp" / "pom.xml").exists() + assert not (tmp_path / "generated" / "myapp" / "generated").exists() + + +# --------------------------------------------------------------------------- +# Structure +# --------------------------------------------------------------------------- + +def test_project_scaffolding_is_where_maven_expects_it(generated_library): + assert (generated_library / "pom.xml").is_file() + # mvnw/mvnw.cmd belong at the project root, only the properties file does not. + assert (generated_library / "mvnw").is_file() + assert (generated_library / "mvnw.cmd").is_file() + assert (generated_library / ".mvn" / "wrapper" / "maven-wrapper.properties").is_file() + assert not (generated_library / ".mvn" / "mvnw").exists() + + assert (generated_library / PACKAGE_DIR / "Library.java").is_file() + assert (generated_library / "src" / "main" / "resources" / "application.properties").is_file() + + +def test_maven_wrapper_is_a_usable_script(generated_library): + mvnw = generated_library / "mvnw" + content = mvnw.read_bytes() + + assert content.startswith(b"#!/bin/sh\n"), "the POSIX wrapper must not carry CRLF endings" + assert b"{{" not in content and b"{%" not in content + if os.name != "nt": + assert os.access(mvnw, os.X_OK), "the POSIX wrapper must be executable" + + +@pytest.mark.parametrize("class_name", ["Library", "Book", "Author"]) +def test_every_concrete_class_gets_a_full_layer_stack(generated_library, class_name): + assert (generated_library / PACKAGE_DIR / "entity" / f"{class_name}.java").is_file() + assert (generated_library / PACKAGE_DIR / "repository" / f"I{class_name}Repository.java").is_file() + assert (generated_library / PACKAGE_DIR / "service" / "interfaces" / f"I{class_name}Service.java").is_file() + assert (generated_library / PACKAGE_DIR / "service" / "impl" / f"{class_name}Service.java").is_file() + assert (generated_library / PACKAGE_DIR / "controller" / f"{class_name}Controller.java").is_file() + + +def test_abstract_class_has_no_repository_service_or_controller(generated_library): + assert (generated_library / PACKAGE_DIR / "entity" / "Person.java").is_file() + assert not (generated_library / PACKAGE_DIR / "repository" / "IPersonRepository.java").exists() + assert not (generated_library / PACKAGE_DIR / "controller" / "PersonController.java").exists() + + +def test_http_samples_stay_out_of_the_compiled_source_tree(generated_library): + assert (generated_library / "src" / "test" / "resources" / "http" / "book.http").is_file() + assert not (generated_library / PACKAGE_DIR / "http_test").exists() + + +# --------------------------------------------------------------------------- +# Content +# --------------------------------------------------------------------------- + +def test_concrete_class_is_mapped_as_an_entity(generated_library): + book = read(generated_library, *PACKAGE_DIR.parts, "entity", "Book.java") + + assert "@Entity" in book + assert '@Table(name = "books")' in book + assert "public class Book {" in book + + +def test_abstract_parent_is_a_mapped_superclass_the_child_extends(generated_library): + person = read(generated_library, *PACKAGE_DIR.parts, "entity", "Person.java") + author = read(generated_library, *PACKAGE_DIR.parts, "entity", "Author.java") + + assert "@MappedSuperclass" in person + assert "@Entity" not in person + assert "public abstract class Person {" in person + assert "public class Author extends Person {" in author + + +def test_many_to_many_has_exactly_one_owning_side(generated_library): + book = read(generated_library, *PACKAGE_DIR.parts, "entity", "Book.java") + author = read(generated_library, *PACKAGE_DIR.parts, "entity", "Author.java") + + assert book.count("@ManyToMany") == 1 + assert author.count("@ManyToMany") == 1 + # Exactly one side declares mappedBy, the other one owns the join table. + mapped_by_sides = [text for text in (book, author) if 'mappedBy = "' in text] + join_table_sides = [text for text in (book, author) if "@JoinTable" in text] + assert len(mapped_by_sides) == 1 + assert len(join_table_sides) == 1 + assert mapped_by_sides[0] is not join_table_sides[0] + assert '@JoinTable(name = "book_author"' in join_table_sides[0] + + +def test_many_to_one_side_owns_the_join_column(generated_library): + book = read(generated_library, *PACKAGE_DIR.parts, "entity", "Book.java") + library = read(generated_library, *PACKAGE_DIR.parts, "entity", "Library.java") + + # Book is the "many" side of Library 1 --- 0..* Book: it holds the FK. + assert "@ManyToOne" in book + assert '@JoinColumn(name = "library_id")' in book + assert "private Library locatedIn;" in book + + # The inverse side is a mapped-by one-to-many, never a second owner. + assert '@OneToMany(mappedBy = "locatedIn")' in library + assert "@JoinColumn" not in library + + +def test_self_association_produces_both_directed_ends(employee_self_assoc_model, tmp_path): + employee = employee_self_assoc_model.get_class_by_name("Employee") + employee.add_attribute(Property(name="id", type=IntegerType, is_id=True)) + SpringBackendGenerator(employee_self_assoc_model, output_dir=str(tmp_path)).generate() + + entity = read(tmp_path, *PACKAGE_DIR.parts, "entity", "Employee.java") + + assert '@OneToMany(mappedBy = "manager")' in entity + assert "private List subordinates" in entity + assert "@ManyToOne" in entity + assert "private Employee manager;" in entity + + +def test_time_type_is_the_same_java_type_in_every_layer(generated_library): + """The regression that made the generated project uncompilable: an entity + field typed ``LocalTime`` with a repository/service method taking + ``LocalDateTime``.""" + entity = read(generated_library, *PACKAGE_DIR.parts, "entity", "Library.java") + repository = read(generated_library, *PACKAGE_DIR.parts, "repository", "ILibraryRepository.java") + service = read(generated_library, *PACKAGE_DIR.parts, "service", "interfaces", "ILibraryService.java") + service_impl = read(generated_library, *PACKAGE_DIR.parts, "service", "impl", "LibraryService.java") + + assert "LocalTime openingTime;" in entity + for layer in (repository, service, service_impl): + assert "findAllByOpeningTime(LocalTime openingTime)" in layer + assert "LocalDateTime" not in layer + + +def test_controller_uses_the_real_identifier_setter(generated_library): + """``Book`` is identified by ``isbn``, so ``setId`` would not exist.""" + controller = read(generated_library, *PACKAGE_DIR.parts, "controller", "BookController.java") + + assert "entity.setIsbn(id);" in controller + assert "entity.setId(id);" not in controller + assert "public ResponseEntity getById(@PathVariable String id)" in controller + + +def test_java_types_are_defined_once(generated_library): + """The four sub-generators must share one mapping, not four copies.""" + from besser.generators.spring import ( + spring_controller_generator, + spring_entity_generator, + spring_repository_generator, + spring_service_generator, + ) + + for module in (spring_entity_generator, spring_repository_generator, + spring_service_generator, spring_controller_generator): + assert not hasattr(module, "JAVA_TYPES"), f"{module.__name__} redefines JAVA_TYPES" + assert JAVA_TYPES["time"] == "LocalTime" + + +# --------------------------------------------------------------------------- +# Determinism +# --------------------------------------------------------------------------- + +def _tree(root: Path) -> list[str]: + return sorted( + str(path.relative_to(root)).replace("\\", "/") + for path in root.rglob("*") if path.is_file() + ) + + +def test_output_is_byte_identical_across_runs(spring_library_model, tmp_path): + """Sets (``enum.literals``, ``model.associations``) iterate in an order that + depends on object identity, so a second, freshly built copy of the same model + is what actually catches unsorted iteration.""" + first, second = tmp_path / "first", tmp_path / "second" + SpringBackendGenerator(spring_library_model, output_dir=str(first)).generate() + SpringBackendGenerator(copy.deepcopy(spring_library_model), output_dir=str(second)).generate() + + assert _tree(first) == _tree(second) + for relative in _tree(first): + assert filecmp.cmp(first / relative, second / relative, shallow=False), relative + + +# --------------------------------------------------------------------------- +# Sanitization +# --------------------------------------------------------------------------- + +def test_traversal_in_a_class_name_stays_inside_the_output_dir(spring_library_model, tmp_path): + evil = Class(name="../../evil") + evil.add_attribute(Property(name="id", type=IntegerType, is_id=True)) + spring_library_model.add_type(evil) + + output_dir = tmp_path / "project" + SpringBackendGenerator(spring_library_model, output_dir=str(output_dir)).generate() + + written = list(tmp_path.rglob("*")) + assert all(output_dir in path.parents or path == output_dir for path in written) + assert not (tmp_path / "evil.java").exists() + assert (output_dir / PACKAGE_DIR / "entity" / "evil.java").is_file() + + +def test_java_keywords_and_odd_names_become_legal_identifiers(): + assert to_java_identifier("../../evil") == "evil" + assert to_java_identifier("a/b\\c") == "a_b_c" + assert to_java_identifier("class") == "class_" + assert to_java_identifier("new", capitalize=True) == "New" + assert to_java_identifier("2fast") == "_2fast" + assert to_java_identifier("..") == "Unnamed" + + +def test_invalid_package_names_are_rejected(): + assert validate_java_package("com.example.app") == "com.example.app" + for invalid in ("../../etc", "com..example", "com.Example", "com.new", ""): + with pytest.raises(ValueError): + validate_java_package(invalid) + + +# --------------------------------------------------------------------------- +# Error reporting +# --------------------------------------------------------------------------- + +def test_missing_identifier_reports_the_class(player_team_domain_model, tmp_path): + """Neither Player nor Team has an ``is_id`` attribute.""" + with pytest.raises(ValueError, match="Player|Team"): + SpringBackendGenerator(player_team_domain_model, output_dir=str(tmp_path)).generate() diff --git a/tests/utilities/web_modeling_editor/backend/test_api_integration.py b/tests/utilities/web_modeling_editor/backend/test_api_integration.py index 7e3298333..b39a13a8f 100644 --- a/tests/utilities/web_modeling_editor/backend/test_api_integration.py +++ b/tests/utilities/web_modeling_editor/backend/test_api_integration.py @@ -9,12 +9,13 @@ do not support the legacy TestClient(app=...) pattern. """ +import copy import io import json import os import asyncio -from functools import wraps -from typing import Any, Dict, Optional +import zipfile +from typing import Any, Dict import pytest import httpx @@ -109,6 +110,26 @@ def class_diagram_model(): } +@pytest.fixture +def spring_class_diagram_input(class_diagram_model): + """``class_diagram_input`` with identifiers, which JPA entities require.""" + model = copy.deepcopy(class_diagram_model) + for class_key, attribute_key in (("class-1", "id-1"), ("class-2", "id-2")): + model["elements"][attribute_key] = { + "type": "Attribute", + "name": "id", + "visibility": "public", + "attributeType": "int", + "isId": True, + } + model["elements"][class_key]["attributes"].append(attribute_key) + return { + "title": "LibraryModel", + "model": model, + "generator": "spring", + } + + @pytest.fixture def class_diagram_input(class_diagram_model): """DiagramInput payload for a class diagram.""" @@ -401,6 +422,66 @@ def test_generate_java_returns_zip(self, class_diagram_input): assert response.status_code == 200 assert "application/zip" in response.headers.get("content-type", "") + def test_generate_spring_applies_the_submitted_config(self, spring_class_diagram_input): + """The Spring dialog's fields must reach the generator, not be dropped.""" + payload = { + **spring_class_diagram_input, + "generator": "spring", + "config": { + "project_name": "mylibrary", + "app_name": "LibraryApplication", + "spring_boot_version": "3.4.4", + "java_version": "21", + "package_name": "com.acme.library", + }, + } + response = client.post("/besser_api/generate-output", json=payload) + assert response.status_code == 200 + assert "application/zip" in response.headers.get("content-type", "") + + with zipfile.ZipFile(io.BytesIO(response.content)) as archive: + names = archive.namelist() + assert "pom.xml" in names + assert "mvnw" in names + assert ".mvn/wrapper/maven-wrapper.properties" in names + # app_name and package_name are what the config asked for. + assert "src/main/java/com/acme/library/LibraryApplication.java" in names + assert "src/main/java/com/acme/library/entity/Book.java" in names + pom = archive.read("pom.xml").decode("utf-8") + assert "3.4.4" in pom + assert "21" in pom + + def test_generate_spring_without_config_uses_defaults(self, spring_class_diagram_input): + """No config at all must still produce a project (the registry default path).""" + payload = {**spring_class_diagram_input, "generator": "spring"} + response = client.post("/besser_api/generate-output", json=payload) + assert response.status_code == 200 + + with zipfile.ZipFile(io.BytesIO(response.content)) as archive: + names = archive.namelist() + assert "src/main/java/com/example/Application.java" in names + + def test_generate_spring_rejects_a_traversing_project_name(self, spring_class_diagram_input): + """``project_name`` is a directory name; it must not escape the temp dir.""" + payload = { + **spring_class_diagram_input, + "generator": "spring", + "config": {"project_name": "../../evil"}, + } + response = client.post("/besser_api/generate-output", json=payload) + assert response.status_code == 200 + with zipfile.ZipFile(io.BytesIO(response.content)) as archive: + assert "pom.xml" in archive.namelist() + + def test_generate_spring_rejects_an_invalid_package_name(self, spring_class_diagram_input): + payload = { + **spring_class_diagram_input, + "generator": "spring", + "config": {"package_name": "../../etc"}, + } + response = client.post("/besser_api/generate-output", json=payload) + assert response.status_code == 400 + # --------------------------------------------------------------------------- # Validation Endpoint -- POST /besser_api/validate-diagram From 6739f9074de84baad8837e3dc3d02901675d2801 Mon Sep 17 00:00:00 2001 From: ArmSL Date: Tue, 22 Sep 2026 12:56:34 +0200 Subject: [PATCH 16/16] test(spring): assert the sanitized class file name with its real casing The traversal test asserted evil.java while the generator correctly writes Evil.java, since a class name is capitalized on the way to a Java identifier. Windows matched it case-insensitively so the assertion passed locally and failed on the Linux CI runners. --- tests/generators/spring/test_spring_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/generators/spring/test_spring_generator.py b/tests/generators/spring/test_spring_generator.py index be19d7d66..3ff4e12a9 100644 --- a/tests/generators/spring/test_spring_generator.py +++ b/tests/generators/spring/test_spring_generator.py @@ -307,8 +307,8 @@ def test_traversal_in_a_class_name_stays_inside_the_output_dir(spring_library_mo written = list(tmp_path.rglob("*")) assert all(output_dir in path.parents or path == output_dir for path in written) - assert not (tmp_path / "evil.java").exists() - assert (output_dir / PACKAGE_DIR / "entity" / "evil.java").is_file() + assert not (tmp_path / "Evil.java").exists() + assert (output_dir / PACKAGE_DIR / "entity" / "Evil.java").is_file() def test_java_keywords_and_odd_names_become_legal_identifiers():