diff --git a/doc/ClassModelDefinition.md b/doc/ClassModelDefinition.md index c2466ba1..498eb27e 100644 --- a/doc/ClassModelDefinition.md +++ b/doc/ClassModelDefinition.md @@ -184,13 +184,19 @@ class University { @Link("uni") List students; + + @Link + Person president; + + @Link + List employees; } ``` -The `@Link` annotation is intended for *bidirectional* associations. +The `@Link` annotation is primarily intended for *bidirectional* associations. The generated code will ensure referential integrity when setting a student's university or when adding or removing students to a university. -*Unidirectional* associations behave no different from attributes, so there is no special annotation for them. +In case you want a *unidirectional* association, you can simply omit the annotation argument, as shown with `president` and `employees` in the `University` example. ```java @@ -238,7 +244,11 @@ public class Student public class University { public static final String PROPERTY_STUDENTS = "students"; + public static final String PROPERTY_PRESIDENT = "president"; + public static final String PROPERTY_EMPLOYEES = "employees"; private List students; + private Person president; + private List employees; public List getStudents() { @@ -304,9 +314,85 @@ public class University return this; } + public Person getPresident() + { + return this.president; + } + + public University setPresident(Person value) + { + this.president = value; + return this; + } + + public List getEmployees() + { + return this.employees != null ? Collections.unmodifiableList(this.employees) : Collections.emptyList(); + } + + public University withEmployees(Person value) + { + if (this.employees == null) + { + this.employees = new ArrayList<>(); + } + if (!this.employees.contains(value)) + { + this.employees.add(value); + } + return this; + } + + public University withEmployees(Person... value) + { + for (final Person item : value) + { + this.withEmployees(item); + } + return this; + } + + public University withEmployees(Collection value) + { + for (final Person item : value) + { + this.withEmployees(item); + } + return this; + } + + public University withoutEmployees(Person value) + { + if (this.employees != null) + { + this.employees.remove(value); + } + return this; + } + + public University withoutEmployees(Person... value) + { + for (final Person item : value) + { + this.withoutEmployees(item); + } + return this; + } + + public University withoutEmployees(Collection value) + { + for (final Person item : value) + { + this.withoutEmployees(item); + } + return this; + } + public void removeYou() { this.withoutStudents(new ArrayList<>(this.getStudents())); + this.setPresident(null); + this.withoutEmployees(new ArrayList<>(this.getEmployees())); } } ``` diff --git a/src/main/java/org/fulib/builder/ClassModelManager.java b/src/main/java/org/fulib/builder/ClassModelManager.java index 0c2ba3f3..e80f8e5e 100644 --- a/src/main/java/org/fulib/builder/ClassModelManager.java +++ b/src/main/java/org/fulib/builder/ClassModelManager.java @@ -664,7 +664,7 @@ public AssocRole associate(Clazz srcClass, String srcRole, int srcSize, Clazz tg * @param srcSize * the cardinality in the source class * @param tgtRole - * the role name in the target class + * the role name in the target class, or {@code null} to make the association unidirectional * @param tgtSize * the cardinality in the target class * @@ -691,7 +691,7 @@ public AssocRole haveRole(Clazz srcClass, String srcRole, Clazz tgtClass, int sr * @param tgtClass * the target class * @param tgtRole - * the role name in the target class + * the role name in the target class, or {@code null} to make the association unidirectional * @param tgtSize * the cardinality in the target class * @@ -716,7 +716,7 @@ public AssocRole haveRole(Clazz srcClass, String srcRole, int srcSize, Clazz tgt * @param tgtClass * the target class * @param tgtRole - * the role name in the target class + * the role name in the target class, or {@code null} to make the association unidirectional * @param tgtSize * the cardinality in the target class * @@ -726,6 +726,11 @@ public AssocRole haveRole(Clazz srcClass, String srcRole, int srcSize, Clazz tgt */ public AssocRole associate(Clazz srcClass, String srcRole, int srcSize, Clazz tgtClass, String tgtRole, int tgtSize) { + if (srcRole == null) + { + throw new NullPointerException("srcRole must not be null"); + } + final AtomicBoolean modified = new AtomicBoolean(false); final AssocRole role = this.haveRole(srcClass, srcRole, srcSize, modified); diff --git a/src/main/java/org/fulib/builder/ReflectiveClassBuilder.java b/src/main/java/org/fulib/builder/ReflectiveClassBuilder.java index 2fd2409e..7d004c4a 100644 --- a/src/main/java/org/fulib/builder/ReflectiveClassBuilder.java +++ b/src/main/java/org/fulib/builder/ReflectiveClassBuilder.java @@ -179,11 +179,18 @@ private static void loadAssoc(Field field, Link link, Clazz clazz, ClassModelMan final String name = field.getName(); final CollectionType collectionType = getCollectionType(field.getType()); - final String otherName = link.value(); + String otherName = link.value(); + if (otherName.isEmpty()) + { + otherName = null; + } final Class other = getOther(field, collectionType); - validateLinkTarget(field.getDeclaringClass(), name, otherName, other); + if (otherName != null) + { + validateLinkTarget(field.getDeclaringClass(), name, otherName, other); + } final String otherClazzName = other.getSimpleName(); final Clazz otherClazz = manager.haveClass(otherClazzName); diff --git a/src/main/java/org/fulib/builder/reflect/Link.java b/src/main/java/org/fulib/builder/reflect/Link.java index 8782eea9..3a8071a5 100644 --- a/src/main/java/org/fulib/builder/reflect/Link.java +++ b/src/main/java/org/fulib/builder/reflect/Link.java @@ -16,6 +16,7 @@ { /** * @return the name of the attribute in the other class. + * An omitted or empty value makes the association unidirectional. */ - String value(); + String value() default ""; } diff --git a/src/main/java/org/fulib/classmodel/AssocRole.java b/src/main/java/org/fulib/classmodel/AssocRole.java index b1bc343c..560bbc4a 100644 --- a/src/main/java/org/fulib/classmodel/AssocRole.java +++ b/src/main/java/org/fulib/classmodel/AssocRole.java @@ -132,7 +132,8 @@ public AssocRole setOther(AssocRole value) public String getId() { final Clazz clazz = this.getClazz(); - return (clazz != null ? clazz.getName() : "_") + "_" + this.getName(); + final String name = this.getName(); + return (clazz != null ? clazz.getName() : "_") + "_" + (name != null ? name : this.getOther().getId()); } public String getName() diff --git a/src/main/java/org/fulib/classmodel/Clazz.java b/src/main/java/org/fulib/classmodel/Clazz.java index c3bdb5fb..711f362d 100644 --- a/src/main/java/org/fulib/classmodel/Clazz.java +++ b/src/main/java/org/fulib/classmodel/Clazz.java @@ -529,6 +529,12 @@ public Clazz withoutAttributes(Collection value) public AssocRole getRole(String name) { + if (name == null) + { + // searching for an anonymous role by name is nonsensical, + // especially since multiple of them can exist. + return null; + } for (AssocRole role : this.getRoles()) { if (Objects.equals(role.getName(), name)) diff --git a/src/main/java/org/fulib/util/Generator4ClassFile.java b/src/main/java/org/fulib/util/Generator4ClassFile.java index af378fab..2f4045e0 100644 --- a/src/main/java/org/fulib/util/Generator4ClassFile.java +++ b/src/main/java/org/fulib/util/Generator4ClassFile.java @@ -141,7 +141,7 @@ private void addDefaultImports(Clazz clazz, Set qualifiedNames) { for (final AssocRole role : clazz.getRoles()) { - if (role.isToMany()) + if (role.getName() != null && role.isToMany()) { this.addCollectionTypeImports(role.getCollectionType(), qualifiedNames); } diff --git a/src/main/resources/org/fulib/templates/associations.javafx.stg b/src/main/resources/org/fulib/templates/associations.javafx.stg index c701e736..a0a3408e 100644 --- a/src/main/resources/org/fulib/templates/associations.javafx.stg +++ b/src/main/resources/org/fulib/templates/associations.javafx.stg @@ -91,12 +91,16 @@ initMethod(role, other) ::= << { for (final value : change.getRemoved()) { + value.; + this.firePropertyChange(PROPERTY_, value, null); } for (final value : change.getAddedSubList()) { + value.; + this.firePropertyChange(PROPERTY_, null, value); } } @@ -105,6 +109,7 @@ initMethod(role, other) ::= << final import(javafx.beans.property.ObjectProperty)\<\> result = new import(javafx.beans.property.SimpleObjectProperty)\<>(); result.addListener((observable, oldValue, newValue) -> { + if (oldValue != null) { oldValue.; @@ -113,6 +118,7 @@ initMethod(role, other) ::= << { newValue.; } + this.firePropertyChange(PROPERTY_, oldValue, newValue); }); return result; diff --git a/src/main/resources/org/fulib/templates/associations.pojo.stg b/src/main/resources/org/fulib/templates/associations.pojo.stg index a6fa5984..954d402e 100644 --- a/src/main/resources/org/fulib/templates/associations.pojo.stg +++ b/src/main/resources/org/fulib/templates/associations.pojo.stg @@ -75,25 +75,25 @@ setMethod(role, other) ::= << public set( value) { + if (this. == value) { return this; } final oldValue = this.; - if (this. != null) { this. = null; oldValue.; } - this. = value; - if (value != null) { value.; } + + this. = value; return this; } @@ -158,12 +158,17 @@ withoutItem(role, other) ::= << public without( value) { + if (this. != null && this..remove(value)) { - value.; - } + + if (this. != null) + { + this..remove(value); + } + return this; } >> diff --git a/src/test/java/org/fulib/builder/ReflectiveClassBuilderTest.java b/src/test/java/org/fulib/builder/ReflectiveClassBuilderTest.java index 27d16076..2abba70a 100644 --- a/src/test/java/org/fulib/builder/ReflectiveClassBuilderTest.java +++ b/src/test/java/org/fulib/builder/ReflectiveClassBuilderTest.java @@ -61,7 +61,11 @@ class University @Link("uni") List students; + @Link() Person president; + + @Link() + List employees; } @Test @@ -136,8 +140,15 @@ public void test() assertThat(uni.getCardinality(), is(Type.ONE)); assertThat(uni.getCollectionType(), nullValue()); - final Attribute president = university.getAttribute("president"); - assertThat(president.getType(), equalTo("Person")); + final AssocRole president = university.getRole("president"); + assertThat(president.getCardinality(), is(Type.ONE)); + assertThat(president.getOther().getName(), nullValue()); + assertThat(president.getOther().getClazz(), is(person)); + + final AssocRole employees = university.getRole("employees"); + assertThat(employees.getCardinality(), is(Type.MANY)); + assertThat(employees.getOther().getName(), nullValue()); + assertThat(employees.getOther().getClazz(), is(person)); } class StringList extends ArrayList diff --git a/src/test/java/org/fulib/generator/AssociationTest.java b/src/test/java/org/fulib/generator/AssociationTest.java index 65053993..ba8070bd 100644 --- a/src/test/java/org/fulib/generator/AssociationTest.java +++ b/src/test/java/org/fulib/generator/AssociationTest.java @@ -6,17 +6,18 @@ import org.fulib.builder.ClassModelBuilder; import org.fulib.builder.Type; import org.fulib.classmodel.ClassModel; +import org.fulib.classmodel.CollectionType; import org.junit.jupiter.api.Test; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; import java.io.File; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; @@ -79,24 +80,28 @@ protected void configureModel(ClassModelBuilder mb) protected final void buildModel(ClassModelBuilder mb) { - ClassBuilder universitiy = mb.buildClass("University").buildAttribute("name", Type.STRING); - // end_code_fragment: - + ClassBuilder university = mb.buildClass("University").buildAttribute("name", Type.STRING); ClassBuilder studi = mb.buildClass("Student").buildAttribute("name", Type.STRING, "\"Karli\""); - - universitiy.buildAssociation(studi, "students", Type.MANY, "uni", Type.ONE); - ClassBuilder room = mb.buildClass("Room").buildAttribute("no", Type.STRING); + ClassBuilder assignment = mb.buildClass("Assignment").buildAttribute("topic", Type.STRING); - universitiy.buildAssociation(room, "rooms", Type.MANY, "uni", Type.ONE) - .setSourceRoleCollection(LinkedHashSet.class).setAggregation(); + // n to 1 + university.buildAssociation(studi, "students", Type.MANY, "uni", Type.ONE); - studi.buildAssociation(room, "condo", Type.ONE, "owner", Type.ONE); + // n to 1 - custom collection type + university + .buildAssociation(room, "rooms", Type.MANY, "uni", Type.ONE) + .setSourceRoleCollection(CollectionType.LinkedHashSet) + .setAggregation(); + // 1 to 1 + studi.buildAssociation(room, "condo", Type.ONE, "owner", Type.ONE); + // n to n studi.buildAssociation(room, "in", Type.MANY, "students", Type.MANY); - ClassBuilder assignment = mb.buildClass("Assignment").buildAttribute("topic", Type.STRING); - studi.buildAssociation(assignment, "done", Type.MANY, "students", Type.MANY); + // unidirectional - the assignment does not need to know who did it + studi.buildAssociation(assignment, "workingOn", Type.ONE, null, 0); + studi.buildAssociation(assignment, "done", Type.MANY, null, 0); } protected void runDataTests(ClassLoader classLoader, String packageName) throws Exception diff --git a/test/src/gen/java/org/fulib/docs/GenModel.java b/test/src/gen/java/org/fulib/docs/GenModel.java index 79999623..11efedb5 100644 --- a/test/src/gen/java/org/fulib/docs/GenModel.java +++ b/test/src/gen/java/org/fulib/docs/GenModel.java @@ -42,6 +42,12 @@ class University { @Link("uni") List students; + + @Link + Person president; + + @Link + List employees; } // end_code_fragment: diff --git a/test/src/main/java/org/fulib/docs/Person.java b/test/src/main/java/org/fulib/docs/Person.java index 6d93f4d4..76940f58 100644 --- a/test/src/main/java/org/fulib/docs/Person.java +++ b/test/src/main/java/org/fulib/docs/Person.java @@ -2,6 +2,8 @@ import java.util.Objects; import java.beans.PropertyChangeSupport; import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; // start_code_fragment: docs.Person public class Person diff --git a/test/src/main/java/org/fulib/docs/University.java b/test/src/main/java/org/fulib/docs/University.java index 8fda3687..138cf569 100644 --- a/test/src/main/java/org/fulib/docs/University.java +++ b/test/src/main/java/org/fulib/docs/University.java @@ -8,7 +8,11 @@ public class University { public static final String PROPERTY_STUDENTS = "students"; + public static final String PROPERTY_PRESIDENT = "president"; + public static final String PROPERTY_EMPLOYEES = "employees"; private List students; + private Person president; + private List employees; public List getStudents() { @@ -74,9 +78,85 @@ public University withoutStudents(Collection value) return this; } + public Person getPresident() + { + return this.president; + } + + public University setPresident(Person value) + { + this.president = value; + return this; + } + + public List getEmployees() + { + return this.employees != null ? Collections.unmodifiableList(this.employees) : Collections.emptyList(); + } + + public University withEmployees(Person value) + { + if (this.employees == null) + { + this.employees = new ArrayList<>(); + } + if (!this.employees.contains(value)) + { + this.employees.add(value); + } + return this; + } + + public University withEmployees(Person... value) + { + for (final Person item : value) + { + this.withEmployees(item); + } + return this; + } + + public University withEmployees(Collection value) + { + for (final Person item : value) + { + this.withEmployees(item); + } + return this; + } + + public University withoutEmployees(Person value) + { + if (this.employees != null) + { + this.employees.remove(value); + } + return this; + } + + public University withoutEmployees(Person... value) + { + for (final Person item : value) + { + this.withoutEmployees(item); + } + return this; + } + + public University withoutEmployees(Collection value) + { + for (final Person item : value) + { + this.withoutEmployees(item); + } + return this; + } + public void removeYou() { this.withoutStudents(new ArrayList<>(this.getStudents())); + this.setPresident(null); + this.withoutEmployees(new ArrayList<>(this.getEmployees())); } } // end_code_fragment: diff --git a/test/src/main/java/org/fulib/docs/classDiagram.png b/test/src/main/java/org/fulib/docs/classDiagram.png index 720d488d..30ae2e68 100644 Binary files a/test/src/main/java/org/fulib/docs/classDiagram.png and b/test/src/main/java/org/fulib/docs/classDiagram.png differ diff --git a/test/src/main/java/org/fulib/docs/classModel.yaml b/test/src/main/java/org/fulib/docs/classModel.yaml index 6955f865..b8e67630 100644 --- a/test/src/main/java/org/fulib/docs/classModel.yaml +++ b/test/src/main/java/org/fulib/docs/classModel.yaml @@ -1,5 +1,5 @@ - c: ClassModel - classes: example university student page person + classes: example university student person page defaultCollectionType: c1 defaultPropertyStyle: POJO defaultRoleType: "java.util.ArrayList<%s>" @@ -19,7 +19,7 @@ modified: false name: University propertyStyle: POJO - roles: university_students + roles: university_students university_president university_employees - student: Clazz model: c @@ -28,18 +28,19 @@ propertyStyle: POJO roles: student_uni -- page: Clazz - attributes: page_lines +- person: Clazz + attributes: person_name person_age model: c modified: false - name: Page + name: Person propertyStyle: POJO + roles: person_University_president person_University_employees -- person: Clazz - attributes: person_name person_age +- page: Clazz + attributes: page_lines model: c modified: false - name: Person + name: Page propertyStyle: POJO - c1: CollectionType @@ -133,6 +134,28 @@ propertyStyle: POJO roleType: "java.util.ArrayList<%s>" +- university_president: AssocRole + aggregation: false + cardinality: 1 + clazz: university + id: University_president + modified: false + name: president + other: person_University_president + propertyStyle: POJO + +- university_employees: AssocRole + aggregation: false + cardinality: 42 + clazz: university + collectionType: c1 + id: University_employees + modified: false + name: employees + other: person_University_employees + propertyStyle: POJO + roleType: "java.util.ArrayList<%s>" + - student_uni: AssocRole aggregation: false cardinality: 1 @@ -143,16 +166,6 @@ other: university_students propertyStyle: POJO -- page_lines: Attribute - clazz: page - collectionType: c1 - id: Page_lines - modified: false - name: lines - propertyStyle: POJO - type: String - typeSignature: String - - person_name: Attribute clazz: person id: Person_name @@ -171,6 +184,38 @@ type: int typeSignature: int +- person_University_president: AssocRole + aggregation: false + cardinality: 0 + clazz: person + collectionType: c1 + id: Person_University_president + modified: false + other: university_president + propertyStyle: POJO + roleType: "java.util.ArrayList<%s>" + +- person_University_employees: AssocRole + aggregation: false + cardinality: 0 + clazz: person + collectionType: c1 + id: Person_University_employees + modified: false + other: university_employees + propertyStyle: POJO + roleType: "java.util.ArrayList<%s>" + +- page_lines: Attribute + clazz: page + collectionType: c1 + id: Page_lines + modified: false + name: lines + propertyStyle: POJO + type: String + typeSignature: String + - c3: CollectionType implClass: class it.unimi.dsi.fastutil.ints.IntArrayList implTemplate: it.unimi.dsi.fastutil.ints.IntArrayList