major
#29807
WrapperGenerator: classifier constants of two enumerations collide, the generated factory does not compile
Affects tl-element 8.0.0-alpha10, com.top_logic.element.model.generate.FactoryGenerator.writeClassifierConstant() / writeClassifier().
Problem
The names of the generated classifier constants and lookup methods are built by concatenating the upper-snake-case of the classifier name and of the enumeration name with a single _:
public static final String LC_FILTER_MEMORY_ADDRESS_SPACE_CLASSIFIER = "LcFilterMemory"; // LcFilterMemory in AddressSpace public static final String LC_FILTER_MEMORY_ADDRESS_SPACE_CLASSIFIER = "LcFilter"; // LcFilter in MemoryAddressSpace
The mangling is ambiguous: classifier LcFilterMemory of enumeration AddressSpace and classifier LcFilter of enumeration MemoryAddressSpace produce the same constant LC_FILTER_MEMORY_ADDRESS_SPACE_CLASSIFIER and the same accessor, so the generated *Factory.java has duplicate members and does not compile. Both names come straight from the KNX schema (LdCtrlMemAddrSpace_t and the address space of a resource location), so a model is not free to avoid them.
Expected
An unambiguous naming, e.g. a separator that cannot occur in a single snake-case name (LC_FILTER__MEMORY_ADDRESS_SPACE_CLASSIFIER, double underscore, as I18NConstants keys do), or constants nested per enumeration.
Workaround
Rename one of the enumerations in the model (here MemoryAddressSpace → MemorySpace).
Seen in the E-Home application (haumacher/ehome, commit 86f15bf, ehome.operational:AddressSpace / ehome.operational:MemorySpace).
Solution
The ambiguity is removed by construction: the metadata of an enumeration moves out of the module factory into a Java class generated per enumeration. The factory keeps object creation and the type and attribute lookups of class types.
Generated class per enumeration. For every TLEnumeration of a module the generator writes a final class named after the enumeration into the module's interface package (e.g. DemoEnum for DemoTypes:DemoEnum):
{{{#!java public final class DemoEnum {
public static final String TYPE_NAME = "DemoEnum";
public static final String FIRST_OPTION_CLASSIFIER = "FirstOption";
public static TLEnumeration type() { ... }
public static TLClassifier firstOptionClassifier() { ... }
} }}}
Since the classifier members live in the class of their enumeration, two enumerations can no longer produce the same member name.
Table name in the generated type interface. The generated base interface of a concrete class type (ABase) declares String TABLE_NAME with the name of the table storing its instances. It is reachable through the business interface as A.TABLE_NAME.
Factory. The generated factory keeps getInstance(), the createX(...) methods, the singleton lookups, the type and attribute lookups get<Type>Type() / get<Part><Type>Attr(), the data type members and the module name constant, which is now called <MODULE>_MODULE (DEMO_TYPES_MODULE). The following members remain as @Deprecated aliases of the members above: <MODULE>_STRUCTURE, KO_NAME_<TYPE>, <ENUM>_ENUM, <CLASSIFIER>_<ENUM>_CLASSIFIER, get<Enum>Enum() and get<Classifier><Enum>Classifier(). A deprecated classifier alias whose name collides with an alias emitted before is skipped, since such a name never compiled.
A nested metadata holder in the type interface (A.Meta.type()) was considered and rejected: a hand-written interface extending two model interfaces (e.g. TLReference extends TLReferenceBase, TLClassPart) inherits two such holders and TLReference.Meta does not compile.
Test. test.com.top_logic.element.model.generate.TestWrapperGenerator generates a model containing the two colliding enumerations of this ticket and compiles the generated sources together with a client of the generated API with -Xlint:deprecation -Werror; the test now fails when the generated code does not compile (it formerly ignored the compiler result). test.com.top_logic.model.util.TestTLModelNamingConvention checks the naming helpers. The generated wrappers of the engine modules are regenerated.
Migration
No change is required: existing code keeps compiling against the deprecated factory members.
After regenerating the wrappers of a module, replace uses of the deprecated factory members:
| = old = | = new = |
| MyFactory.MY_MODULE_STRUCTURE | MyFactory.MY_MODULE_MODULE |
| MyFactory.KO_NAME_A | A.TABLE_NAME |
| MyFactory.MY_ENUM_ENUM | MyEnum.TYPE_NAME |
| MyFactory.FIRST_MY_ENUM_CLASSIFIER | MyEnum.FIRST_CLASSIFIER |
| MyFactory.getMyEnumEnum() | MyEnum.type() |
| MyFactory.getFirstMyEnumClassifier() | MyEnum.firstClassifier() |
A module whose factory did not compile because of colliding classifier names compiles after regeneration; the colliding legacy aliases are not generated.