Skip to content

MetaModels

homedirectory edited this page Sep 21, 2023 · 41 revisions

Meta-Models, Annotation Processing and m2e

Contents

  1. Abstract
  2. Background
  3. Initial setup
  4. Ongoing usage
  5. Using meta-models
    1. Introduction
    2. Dot notation
    3. Annotations
    4. Non-persistent entities
    5. Viewing progress in Eclipse
  6. Further reading
  7. TODO

Abstract

This document provides a few notes on configuring a TG-based application to use annotation processing to generate meta-models in Eclipse.

These examples will use TG Fleet - adjust as required for other projects.

In order to simplify the management of application projects, which use meta-models in Eclipse IDE, it is highly recommended to embrace the m2e plugin instead of Maven to clean and generate Eclipse project files. Effectively, the use of m2e provides a way to retain some of the important configurations, which are required for generation of meta-models. This documentation shows the use of m2e.


Background

Entities declare properties which are reified using String values for purposes such as definining a fetch model.

For example, entity Task is declared with properties name, desc, detailingTask, and the respective fetch provider could be declared like:

static final IFetchProvider<Task> FETCH_PROVIDER = EntityUtils.fetch(Task.class).with("name", "desc", "detailingTask", ...);

Any changes to the model, such as renaming of properties, would require the developer to manually adjust all String-typed property references, which tends to be highly error-prone. With the use of meta-models the job of finding references that need adjusting is delegated to the compiler, making this a simple process of following the compilation errors.

Meta-models themselves are represented by Java classes which are generated at compile time. Property references can be constructed by chaining the respective method calls on meta-model objects. So instead of referring to property detailingTask with string literal "detailingTask", you would use the Task meta-model: Task_.detailingTask(), which can be further converted to a String if needed: Task_.detailingTask().toPath(). The .toPath() method converts a meta-model (IConvertableToPath) to String, and is only required where IConvertableToPath is not accepted.

A meta-model's class name is the same as its underlying entity, but with a MetaModel suffix. All meta-models are listed in the generated container class MetaModels which serves as an entry point to access them. Each meta-model can be accessed in 2 ways from MetaModels:

  1. Through a static field that's named after the underlying entity with a _ suffix (e.g., MetaModels.Task_ represents the TaskMetaModel).
  2. Through a static method that accepts an alias (e.g., MetaModels.Task_("t")). These are called aliased meta-models, and the paths they convert to are prefixed with the respective alias. For example, MetaModels.Task_("t").desc() converts to "t.desc".

Additionally, a bare meta-model reference, such as Task_, converts to "this". A bare aliased meta-model reference, such as Task_("t"), converts to its alias ("t").

Using meta-models to reference properties enables auto-completion of property names and potentially compile-time checks on property validity (by leveraging the type system). It also supports dot notation, and provides ready access to comprehensive javadoc describing the properties.


Initial setup

Preparing Eclipse IDE

Apart from a standard Java compiler, Eclipse provides a "batch compiler" that is capable of processing source files in batches. This batch mode is triggered whenever the number of input source files exceeds a certain limit (by default set to 2000). It is required to disable it by removing the limit, since there are known issues with annotation processing in batch mode. To do so, navigate to your local Eclipse installation and modify the eclipse.ini file by adding the following:

-DmaxCompiledUnitsAtOnce=0

Make sure that this line comes after -vmargs.

Project-specific settings

It is necessary to install the annotation processor jar file, and to perform an initial configuration of the project. It is envisaged that most of the necessary configuration changes would be committed to GitHub, and thus available to anyone who clones or pulls the project.

  1. It is generally recommended to show working sets at the top level - click the button in the Package Explorer toolbar with three vertical dots and select Top Level Elements - Working Sets.

  2. Ensure that there are no uncommitted changes in the target project (e.g. TG Fleet) - to ensure a clean start, the project will be freshly cloned from GitHub to avoid any side-effects that might linger from previous Eclipse operations.

  3. Open Eclipse and delete the TG Fleet modules, and then the respective working set (do this as two separate actions, as deleting the working set first effectively hides the project modules, preventing you from re-importing them).

    You can elect to delete the files from disk now (tick the Delete project contents on disk (cannot be undone) checkbox), or do not, and archive/remove them manually (in case something goes wrong).

  4. Quit Eclipse.

  5. Checkout or update tg. At the time of writing, switch to branch migration-to-java-11-with-EQL3 (suitable for modern Java only).

  6. Perform mvn clean install on tg - this should install the annotation processor jar in your .m2 directory.

    Note: At the time of writing, this has the same version number as the platform - 1.4.6-SNAPSHOT.

  7. Clone a fresh copy of TG Fleet into your Eclipse workspace e.g. git clone git@github.com:fieldenms/tgfleet.git.

    Alternatively if you have chosen not to delete your existing TG Fleet project, remove all Eclipse-related artifacts (mvn clean eclipse:clean might be a good place to start).

  8. Create file fleet-pojo-bl/.factorypath (if it does not already exist) with the following content:

    <factorypath>
        <factorypathentry kind="VARJAR" id="M2_REPO/fielden/platform-annotation-processors/1.4.6-SNAPSHOT/platform-annotation-processors-1.4.6-SNAPSHOT.jar" enabled="true" runInBatchMode="false"/>
    </factorypath>

    Note that this is pointing to the jar file installed in an earlier step.

  9. In fleet-pojo-bl/pom.xml add the following text to the <build>...</build> section (if it is not already there):

         <plugins>
             <plugin>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
                     <encoding>UTF-8</encoding>
                     <!-- to enable output of annotation processors -->
                     <showWarnings>true</showWarnings>
                     <generatedSourcesDirectory>${project.build.directory}/generated-sources/</generatedSourcesDirectory>
                     <annotationProcessorPaths>
                         <annotationProcessorPath>
                             <groupId>fielden</groupId>
                             <artifactId>platform-annotation-processors</artifactId>
                             <version>${platform.version}</version>
                         </annotationProcessorPath>
                     </annotationProcessorPaths>
                     <annotationProcessors>
                         <annotationProcessor>
                             ua.com.fielden.platform.processors.verify.VerifyingProcessor
                         </annotationProcessor>
                         <annotationProcessor>
                             ua.com.fielden.platform.processors.metamodel.MetaModelProcessor
                         </annotationProcessor>
                     </annotationProcessors>
                 </configuration>
             </plugin>
         </plugins>
  10. Start Eclipse.

  11. From the menu select File - Import.... In the import dialog expand Maven and select Existing Maven Projects. Click Next >.

  12. In the Import Maven Projects dialog browse to the tgfleet/fleet directory inside your Eclipse workspace (where the top-level pom.xml resides).

    The dialog should show the top level pom.xml, and the pom.xml for each of the modules under it, including fleet-pojo-bl (like unto searching for nested projects when importing a Maven-generated Eclipse project file).

    Note: If the pom.xml appears in the list but is greyed out and nothing can be selected, it is likely that the TG Fleet modules still exist in Eclipse. Cancel the import, view all projects, remove those related to TG Fleet, then try again.

    Note: The default working set name is the same as the directory name i.e. fleet. You can change it to match the GitHub repository name tgfleet if you like.

  13. Click Finish.

    Note: Importing could take several minutes, and will perform a number of steps, including Importing Maven projects, Discover lifecycle mappings and Download sources and javadoc.

    Note: During import while preparing these notes, a "warning" about discovering m2e connectors was displayed with no actual error message. Clicking Finish produced a warning that "projects will have build errors", and "see help for more information". OK was clicked and the import finished successfully with no errors.

  14. This should result in a top level working set tgfleet (or just fleet if you did not change the default), with the separate modules below, including the top-level fleet.

  15. Select the fleet-pojo-bl module in the package Explorer, right-click and select Properties.

    1. Expand Java Compiler and select Annotation Processing.

    2. Enable project specific settings (click the first checkbox).

    3. Specify Generated source directory as target/generated-sources.

    4. Specify Generated test source directory as target/generated-test-sources.

    5. The following processor options are supported:

      1. cacheStats - setting true enables recording of cache statistics and reporting of additional messages. This might be useful for troubleshooting or measuring performance.

      When specifying options do not use prefix -A before the option name, as hinted by Eclipse.

    6. Click Apply and Close.

    7. A dialog Annotation Processing Settings Changed should appear - click Yes to rebuild the project.

  16. If this all worked, you should be able to open type MetaModels - press your "open type" keyboard shortcut (⌘⇧T on macOS), and filter by MetaModels - you should see a class in fleet-pojo-bl/target/generated-sources - select it and click Open.


Ongoing usage

When working on a meta-model-enabled project, the following guidelines should be followed for project maintenance.

  1. Update the source, typically with a git pull.

  2. Open Eclipse.

  3. Select the tgfleet working set (or just fleet if you did not change the default name when importing the project), press F5 to refresh, then right-click and select Maven - Update Project....

    Note: A keyboard shortcut may be easier - on macOS the appropriate shortcut is ⌥F5, which follows on nicely from the F5 to refresh.

  4. In the Update Maven Project window ensure that all modules are selected and the three checkboxes to the lower left are checked, as illustrated below, then click OK.

    This will go through the steps of updating the Maven project, downloading source (if required), and then building.


Using meta-models

Introduction

Anywhere that a string literal is used to represent an entity's property, it should be replaced with the equivalent meta-model reference.

For example TaskCo.java could be adjusted as follows:

package fielden.work;

import static metamodels.MetaModels.Task_;

public interface TaskCo extends IEntityDao<Task>, ICanAttach<Task, TaskAttachment> {

    /* Old version:
    static final IFetchProvider<Task> FETCH_PROVIDER = EntityUtils.fetch(Task.class).with(
            "name", "desc", "active", "estimatedManhours", "totalItems",
            "mainTask", "inspectionTask", "detailingTask", "numberOfAttachments");
    */
    static final IFetchProvider<Task> FETCH_PROVIDER = EntityUtils.fetch(Task.class).with(
            Task_.name(), Task_.desc(), Task_.active(), Task_.estimatedManhours(), Task_.totalItems(),
            Task_.mainTask(), Task_.inspectionTask(), Task_.detailingTask(), Task_.numberOfAttachments());

}

Note the static import near the top of the file.

The old version of the fetch provider, with string literals, is retained for comparison purposes. It can be seen that the meta-model version is longer, but only slightly so considering the advantages this approach offers. You might consider putting each property in the fetch model onto a separate line - it has been shown to improve readability a little, but may not be appropriate if there are a large number of properties.

Suppose that the detailingTask property was to be renamed to detailersTask.

Pre-meta-model, you would edit Task.java and rename the property, then search through all source files to find references to detailingTask and change them to detailersTask, hoping that you had not missed any. Some would be detected at runtime (e.g. selection criteria and EGI properties in a webui), but others might not (e.g. an EQL query that is only executed under specific circumstances).

Post-meta-model, as soon as Task.detailingTask is refactored/renamed to Task.detailersTask, the meta-models are re-generated, and any existing references to Task_.detailingTask() are immediately flagged as compile-time errors.

Note that MetaModels class fields that expose meta-models are named after the underlying entities they represent with an underscore at the end. So class Task would be represented by a meta-model in the field Task_.

This prevents simple name conflicts in case where the underlying entity class is also imported. For example, TaskWebUiConfig imports both Task and its meta-model (Task_ through a static import).

import fielden.work.Task;
import static metamodels.MetaModels.Task_;

final EntityCentreConfig<Task> ecc = EntityCentreBuilder.centreFor(Task.class)
        ...
        .addCrit(Task_).asMulti().autocompleter(Task.class).also()
        .addCrit(Task_.active()).asMulti().bool().setDefaultValue(multi().bool().setIsValue(true).setIsNotValue(false).value()).also()
        .addCrit(Task_.desc()).asMulti().text().also()
        .addCrit(Task_.estimatedManhours()).asRange().decimal().also()
        .addCrit(Task_.totalItems()).asRange().integer().also()
        .addCrit(Task_.numberOfAttachments()).asRange().integer().also()
        .addCrit(Task_.mainTask()).asMulti().bool().also()
        .addCrit(Task_.inspectionTask()).asMulti().bool().also()
        .addCrit(Task_.detailingTask()).asMulti().bool()
        ...
        .addProp(Task_).order(1).asc().minWidth(100)
            .withSummary("total_count_", "COUNT(SELF)", format("Count:The total number of matching %ss.", Task.ENTITY_TITLE))
            .withAction(editTaskAction).also()
        .addProp(Task_.desc()).minWidth(200).also()
        .addProp(Task_.estimatedManhours()).minWidth(100).also()
        .addProp(Task_.totalItems()).minWidth(100).withAction(editTaskAction).also()
        .addProp(Task_.numberOfAttachments()).minWidth(100).withAction(editTaskAction).also()
        .addProp(Task_.mainTask()).minWidth(64).also()
        .addProp(Task_.inspectionTask()).minWidth(64).also()
        .addProp(Task_.detailingTask()).minWidth(64).also()
        .addProp(Task_.active()).minWidth(100)
        .addPrimaryAction(editTaskAction)
        .build();

Dot notation

Meta-models allow dot-notation, as well as providing javadoc describing the properties that are traversed. For example, in WorkOrderWebUiConfig the following image illustrates replacing "station.zone.sector.division" with the meta-model equivalent:

At the point when the screen capture was taken, the user had typed WorkOrder_.station().zone().sector().d and pressed Ctrl-space, and Eclipse displayed the available properties. This included division, along with javadoc describing all of the essential attributes from the property definition.

Now any change to any of the entities or properties used here would immediately result in a compilation error, highlighting the affected areas of the reference that would need to be adjusted.

Due to the scope of work, it is not envisaged that all string-based property references will be changed at once. Instead it is proposed that anyone working in a particular file where string-based property names are used would take a few moments to update that file to use meta-models. Over time, the entire application will gradually be transformed.

Annotations

Up to a point, meta-models can be used in annotations in entity definitions, such as @Dependent, as these references are almost always a simple string property name in the same entity.

Such meta-model property names are referenced statically via a meta-model class name rather than an instance name.

For example, in entity OperationCheck there is property checkStartDate, which has dependent property checkFinishDate, which could be declared like this:

import static fielden.fleet.meta.OperationCheckMetaModel.checkFinishDate_;

    [...]

    @IsProperty
    @MapTo
    @DateOnly
    @Dependent(checkFinishDate_) // compile-time constant value
    @Title(value = "Check Start Date", desc = "The date when the operation check started.")
    private Date checkStartDate;

This is considered the only valid use of String-type meta-model property references - all other references should use the IConvertableToPath form e.g. OperationCheck_.checkFinishDate() (followed by .toPath() if absolutely necessary).

Non-persistent entities

Meta-models are generated for all domain entities at the application level. However, they are not generated for some kinds of entities, including synthetic and union entities.

In order to generate meta-models for such entities, annotate them with @DomainEntity or @WithMetaModel, i.e. add one of these annotations just above the top of the class definition, where other annotations like @EntityTitle are declared. @DomainEntity is intended for synthetic and union entities, which can be considered to be a part of the domain. Conversely, @WithMetaModel should be used for non-domain entities, such as locators or functional entities (aka actions).

Note that typing the annotation and saving the file should trigger automatic re-generation of meta-models, now including the one just annotated.

For entities defined at the platform level, such as User and Attachment, the use of @DomainEntity is required irrespective of whether the entities are persistent or not. This was a design decision due to existence of a large number of various "test" entities at the platform level that would otherwise pollute meta-models at the application level.

Type Money is a special case that does not have a meta-model, but it might at times be required to access .amount. If the need or a benefit is identified for having a meta-model for Money, the annotation processor can always be enhanced to generate one.

Viewing progress in Eclipse

It is possible to view the progress of annotation processing in Eclipse, although it generally takes less than a second to complete.

From the Eclipse main menu, select Window - Show View - Error Log.


Further reading


TODO

  • Nothing much.
Clone this wiki locally