What this is about
You do not write an entity, a DTO, or a controller for a model. The code generator does that in every build. It reads the metadata from the hub and generates all the classes a model needs, from the REST interface down to the database.
The generator is an annotation processor. That is a program the Java compiler calls while compiling, and it is allowed to write new source files itself. The compiler compiles those new files in the same run.
The flow
flowchart LR
H[("Hub")]
subgraph GS["Phase generate-sources"]
F["Fetch metadata<br/>CdmsMetadataFetcher"]
C[["YAML cache<br/>system, folders,<br/>models, enumerations"]]
P["Maintain POM"]
end
subgraph CO["Phase compile"]
G["Generator<br/>(annotation processor)"]
J["javac"]
E["Enhance entities<br/>(Hibernate)"]
end
subgraph PK["Phase package"]
B["Spring Boot JAR<br/>or native image"]
end
H -- "REST + token" --> F
F --> C
F --> P
C --> G
G -- "target/generated-sources" --> J
J --> E --> B
You inherit the whole flow from the parent cdms-app-parent. None of it appears in your own pom.xml. The profile becomes active as soon as src/main/java exists.
-
1Build→Keycloakgets a token (client credentials) using the client ID and secret from the configuration
-
2Build→Hubreads the system and fetches folders, models, fields, hooks, endpoints, and enumerations page by page
-
3Buildputs everything together and writes the YAML cache:
system.yaml,folders.yaml,models.yaml,enumerations.yaml -
4Buildmaintains the
pom.xmlaccording to the system's settings, see project scaffold -
5Generatorreads the cache and generates the classes of all models into
target/generated-sources/annotations/The generator reads only the cache, not any annotations in the code. The Java package of a class is the base package plus the folder path from the hub. -
6Buildcompiles the generated code and your custom code together, and enhances the entitiesResult: Finished classes in
target/classes, ready to package
Online, offline, skip
When: the normal case
The build gets a token and reads the metadata fresh from the hub. After that, the cache holds the current state.
Result: Code matches the model in the hub
When: CODEGEN_OFFLINE=true
No token, no call to the hub. The build requires all four cache files to be present and generates from them. If one is missing, it stops with a hint to build once with CODEGEN_OFFLINE=false. The POM is still maintained.
Result: Code matches the state in the cache
When: -Dcdms.generator.fetch.skip=true
The whole "fetch metadata" step is skipped, and so is the POM maintenance. The generator then only needs models.yaml in the cache. This lets a fresh project scaffold build the first time, without a secret.
Result: Code matches the state in the cache
When: you want to create the cache without a build
GET /api/rest/cms/system/{id}/export returns a ZIP with the four YAML files. Unzip it into the cache directory, and you can then build offline. The project scaffold also already contains this state under .cache/.
The configuration
The build needs a few settings. It looks for them in this order and takes the first match:
-
BuildEnvironment variable
CODEGEN_CLIENT_SECRET -
BuildDerived environment variable
CDMS_GENERATOR_CLIENT_SECRET -
BuildJVM property
-Dcdms.generator.clientSecret=… -
BuildFile in the project
clientSecret=…incdms-generator.properties - otherwise the default value
| Setting | Environment variable | Default |
|---|---|---|
Issuer of the generator’s token, with /realms/<realm> | CODEGEN_KEYCLOAK_ISSUER | – (required) |
| Client ID / secret | CODEGEN_CLIENT_ID, CODEGEN_CLIENT_SECRET | – (required) |
| Hub address | CODEGEN_CDMS_URL | set in the project scaffold |
| System ID | CODEGEN_SYSTEM_ID | – (required) |
| Cache directory | CODEGEN_CACHE_DIR | target/cache (.cache in the project scaffold) |
| offline | CODEGEN_OFFLINE | false |
| Base package | CODEGEN_BASE_PACKAGE | com.codamai.cdms |
| Maintain POM | CODEGEN_POM_GENERATE | true |
Only the online path needs the required settings. Offline, the cache directory and the base package are enough.
What is generated per model
A normal model gets 15 classes, an abstract model gets 10. The name is always the model name plus a suffix. For a model Order:
OrderApi– the REST controllerOrderCreatePayloadOrderUpdatePayloadOrderPayload2DtoMapper
OrderSystem– the flow of reading and writingOrderDtoOrderMetaService– the metadataOrderDto2EntityMapperOrderMapperService
OrderAuthorizationLayer– checks rolesOrder{Field}Filter– one per access filter
OrderEntityOrderDatabaseOrderTupleMapperServiceOrderEntity2DtoMapperOrderMap2EntityMapper
| Case | Classes |
|---|---|
| normal model | 15, plus one per access filter |
| singleton | 15. Api and System are the singleton variants without id in the path |
| abstract model | 10. System, MapperService, AuthorizationLayer, Database, TupleMapperService are missing because it has no objects of its own. Its Api searches across all subtypes |
| enumeration | a Java enum with the values |
How the shapes payload, DTO, entity, and meta work together is shown in One model, four shapes.
What is generated once per build and once ever
| File | Location | When |
|---|---|---|
| Classes per model and enumeration | target/generated-sources/ | fresh in every build |
RoleRegistryService, EntityRegistryService, JpaNativeHints, JacksonNativeHints | target/generated-sources/ | once per build, across all models |
Start.java, OpenApiConfiguration, NativeHintsConfiguration | src/main/java/ | only if the file is missing, never again after that |
pom.xml, the section between the cdms-managed markers | Project root | updated in every online or offline build |
RoleRegistryService knows all roles and attributes of all models and provides them to CIAS as a declaration, see Declaring roles. EntityRegistryService knows all entities. The two …NativeHints tell the native image compiler which classes it needs at runtime via reflection.