Skip to content

Commit

Permalink
Merge pull request #81 from fujaba/fix/unidirectional-link
Browse files Browse the repository at this point in the history
Proper support for unidirectional associations
  • Loading branch information
Clashsoft authored Nov 14, 2020
2 parents f0cd3d5 + 8fb0e02 commit 1a3d8f2
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 49 deletions.
90 changes: 88 additions & 2 deletions doc/ClassModelDefinition.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,19 @@ class University
{
@Link("uni")
List<Student> students;

@Link
Person president;

@Link
List<Person> employees;
}
```
<!-- end_code_fragment: -->

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.

<!-- insert_code_fragment: docs.Student | fenced:java -->
```java
Expand Down Expand Up @@ -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<Student> students;
private Person president;
private List<Person> employees;

public List<Student> getStudents()
{
Expand Down Expand Up @@ -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<Person> 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<? extends Person> 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<? extends Person> 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()));
}
}
```
Expand Down
11 changes: 8 additions & 3 deletions src/main/java/org/fulib/builder/ClassModelManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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
*
Expand All @@ -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
*
Expand All @@ -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);
Expand Down
11 changes: 9 additions & 2 deletions src/main/java/org/fulib/builder/ReflectiveClassBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/fulib/builder/reflect/Link.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
}
3 changes: 2 additions & 1 deletion src/main/java/org/fulib/classmodel/AssocRole.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/fulib/classmodel/Clazz.java
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,12 @@ public Clazz withoutAttributes(Collection<? extends Attribute> 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))
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/fulib/util/Generator4ClassFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ private void addDefaultImports(Clazz clazz, Set<String> qualifiedNames)
{
for (final AssocRole role : clazz.getRoles())
{
if (role.isToMany())
if (role.getName() != null && role.isToMany())
{
this.addCollectionTypeImports(role.getCollectionType(), qualifiedNames);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,16 @@ initMethod(role, other) ::= <<
{
for (final <other.clazz.name> value : change.getRemoved())
{
<if(role.other.name)>
value.<withoutThis(other)>;
<endif>
this.firePropertyChange(PROPERTY_<role.name; format="upper_snake">, value, null);
}
for (final <other.clazz.name> value : change.getAddedSubList())
{
<if(role.other.name)>
value.<withThis(other)>;
<endif>
this.firePropertyChange(PROPERTY_<role.name; format="upper_snake">, null, value);
}
}
Expand All @@ -105,6 +109,7 @@ initMethod(role, other) ::= <<
<else>
final import(javafx.beans.property.ObjectProperty)\<<other.clazz.name>\> result = new import(javafx.beans.property.SimpleObjectProperty)\<>();
result.addListener((observable, oldValue, newValue) -> {
<if(role.other.name)>
if (oldValue != null)
{
oldValue.<withoutThis(other)>;
Expand All @@ -113,6 +118,7 @@ initMethod(role, other) ::= <<
{
newValue.<withThis(other)>;
}
<endif>
this.firePropertyChange(PROPERTY_<role.name; format="upper_snake">, oldValue, newValue);
});
return result;
Expand Down
15 changes: 10 additions & 5 deletions src/main/resources/org/fulib/templates/associations.pojo.stg
Original file line number Diff line number Diff line change
Expand Up @@ -75,25 +75,25 @@ setMethod(role, other) ::= <<
<endif>
public <role.clazz.name> set<role.name; format="cap">(<other.clazz.name> value)
{
<if(other.name)>
if (this.<role.name> == value)
{
return this;
}

final <other.clazz.name> oldValue = this.<role.name>;
<if(other.name)>
if (this.<role.name> != null)
{
this.<role.name> = null;
oldValue.<withoutThis(other)>;
}
<endif>
this.<role.name> = value;
<if(other.name)>
if (value != null)
{
value.<withThis(other)>;
}
<else>
this.<role.name> = value;
<endif>
return this;
}
Expand Down Expand Up @@ -158,12 +158,17 @@ withoutItem(role, other) ::= <<
<endif>
public <role.clazz.name> without<role.name; format="cap">(<other.clazz.name> value)
{
<if(other.name)>
if (this.<role.name> != null && this.<role.name>.remove(value))
{
<if(other.name)>
value.<withoutThis(other)>;
<endif>
}
<else>
if (this.<role.name> != null)
{
this.<role.name>.remove(value);
}
<endif>
return this;
}
>>
Expand Down
15 changes: 13 additions & 2 deletions src/test/java/org/fulib/builder/ReflectiveClassBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ class University
@Link("uni")
List<Student> students;

@Link()
Person president;

@Link()
List<Person> employees;
}

@Test
Expand Down Expand Up @@ -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<String>
Expand Down
33 changes: 19 additions & 14 deletions src/test/java/org/fulib/generator/AssociationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 1a3d8f2

Please sign in to comment.