fieldenms / tg

Trident Genesis
MIT License
14 stars 7 forks source link

Meta-model: compile-time entity meta-model generation #849

Closed 01es closed 2 years ago

01es commented 7 years ago

Description

Instead of relying on very unreliable and error prone (especially after renaming) string-based referencing of entity properties (including dot-notations for graph traversal) as part of various DSL calls such as in EQL, fetch and Web UI configurations, it is required to develop a support for an ad-hoc generation of a programming model that would provide a way for reliably reference entity properties. Compile time errors should be reported in cases where non-existing properties are referenced.

The implementation should analyse the domain entities and generate a set of classes with static members resembling the graph structure those entities. The developers should be equipped then to use those static member to travers the entity graph in the code editor. For example, typing something like $model.WorkActivity.locationSubsystem. should result in the autocompletion (code inside) of all properties of entity LocationSubsystem, which is the type of property locationSubsystem in entity WorkActivity.

There are two approaches to triggering of the property-model generation.

  1. Have a class with method main that would trigger (re)generation of $model.
  2. Support automatic (re)generation upon changes to entities by implementing an approach similar to that in the Lobmbok project.

Expected outcome

This feature will further increase evolvability of TG-based applications whereby developers will be informed by the compiler of invalid references to entity properties.

homedirectory commented 2 years ago

Some entities, such as User, may contain properties that are of the same type as the owning entity. For example, here is one of the properties of an entity User:

@IsProperty
@Title(value = "Base User", desc = "...")
@MapTo
@BeforeChange(@Handler(UserBaseOnUserValidator.class))
@AfterChange(UserBasedOnUserDefiner.class)
private User basedOnUser;

The generated meta-model reflects this property as such:

public final UserMetaModel basedOnUser;

and initializes it in the constructor like so:

this.basedOnUser = new UserMetaModel(joinPath(basedOnUser_));

The problem is that the UserMetaModel object can't be instantiated, since the initial constructor call will never terminate, causing a StackOverflowError:

java.lang.StackOverflowError
        at ua.com.fielden.platform.security.user.meta.UserMetaModel.<init>(UserMetaModel.java:108)
        at ua.com.fielden.platform.security.user.meta.UserMetaModel.<init>(UserMetaModel.java:114)
        at ua.com.fielden.platform.security.user.meta.UserMetaModel.<init>(UserMetaModel.java:114)
        at ua.com.fielden.platform.security.user.meta.UserMetaModel.<init>(UserMetaModel.java:114)
        ....
        at ua.com.fielden.platform.security.user.meta.UserMetaModel.<init>(UserMetaModel.java:114)

The solution is to utilize lazy initialization of such properties in the generated meta-model class through the use of java.util.function.Supplier. Consequently, instead of providing direct access to properties of the generated meta-model class, getter-like methods should be provided with equivalent names.

class UserMetaModel {
    private static final String basedOnUser_ = "basedOnUser";

    private Supplier<UserMetaModel> basedOnUser;

    public UserMetaModel(String path) {
        super(path);
        this.basedOnUser = () -> {
            UserMetaModel value = new UserMetaModel(joinPath(basedOnUser_));
            basedOnUser = () -> value;
            return value;
        };
    }

    public UserMetaModel basedOnUser() {
        return basedOnUser.get();
    }
}
homedirectory commented 2 years ago

Cyclic reference during initiazliation of a meta-model class.

Suppose entity Person has a property vehicle: Vehicle and entity Vehicle has a property owner: Person. Then the following constructors would be generated:

class PersonMetaModel {
...
    public PersonMetaModel(String path) {
        super(path);
        this.vehicle = new VehicleMetaModel(joinPath(vehicle_));
    }
}
class VehicleMetaModel {
...
    public VehicleMetaModel(String path) {
        super(path);
        this.owner = new PersonMetaModel(joinPath(owner_));
    }
}

This leads to an infinite loop.


I see 2 possible solutions. In the process of meta-model generation we have to check if a property causes a cyclic reference, that is, whether a property is entity type and that entity has a property of current (generation target) entity type.

Example for Vehicle and Person:

  1. Instead of assigning a value to Person.vehicle inside the constructor, create a setter method setVehicle() and call it in MetaModels class:
class MetaModels {
...
    public static final PersonMetaModel Person = new PersonMetaModel().setVehicle();
}
  1. Employ the same approach as for entities that have properties of their own type, such as User.basedOnUser. a. Combine this with above mentioned check for cyclic properties. b. Do this for every property that is entity type.
01es commented 2 years ago

@homedirectory approach 2 with lazy initialisation should work well as the current situation is not really different from the original one. Consider employing lazy initialisation in all cases for entity-typed properties, regardless of whether they are recursively defined.

homedirectory commented 2 years ago

Issues caused by entity inheritance and the incremental compilation process of Eclipse.

Eclipse uses its own JDT core as the Java compiler providing incremental compilation. This leads to an issue when recompiling entity classes that have child entities classes, since the child classes are not automatically recompiled together with their parent. Therefore the annotation processor's environment consists of a sole parent class.

In order to resolve this issue whe have to recreate the same inheritance tree for the generated sources, i.e. ChildMetaModel extends ParentMetaModel.

01es commented 2 years ago

@homedirectory I think this is due to binary compatibility (https://docs.oracle.com/javase/specs/jls/se17/html/jls-13.html), which makes it possible for child classes (compiled) to remain unchanged if their parent changes (e.g., some parent class from a 3rd party library) -- something that completely escaped my mind when we were considering this aspect.

Please let me know if you would like to discuss the proposed by your approach to remedy this situation.