Skip to content

Latest commit

History

History
435 lines (360 loc) · 19.6 KB

File metadata and controls

435 lines (360 loc) · 19.6 KB

Java port

The Java port targets Spring-Boot consumers on Maven. It ships the full metamodel

  • loader + conformance + OMDB runtime persistence engine + the FR-004 render engine, plus the metaobjects-maven-plugin for build-time codegen (mvn metaobjects:generate / metaobjects:editor).

Schema migrations are owned by the TypeScript toolchain (@metaobjectsdev/cli migrate); the Java diff-and-converge migration engine and its meta:migrate / live-DB-drift metaobjects:verify Maven goals were removed. Per ADR-0015 the OMDB runtime auto-create path was also removed — OMDB is pure data-access (CRUD/query/codec/transactions). Prompt / template drift is still checked via the metaobjects-renderVerify API.

Install

Set ${metaobjects.version} to the current Maven Central release (7.23.0) — both the dependency and plugin blocks below resolve it from one <properties> entry:

<!-- pom.xml -->
<properties>
<metaobjects.version>7.23.0</metaobjects.version>
</properties>
<dependencies>
<dependency>
<groupId>com.metaobjects</groupId>
<artifactId>metaobjects-metadata</artifactId>
<version>${metaobjects.version}</version>
</dependency>
<dependency>
<groupId>com.metaobjects</groupId>
<artifactId>metaobjects-omdb</artifactId>
<version>${metaobjects.version}</version>
</dependency>
<dependency>
<groupId>com.metaobjects</groupId>
<artifactId>metaobjects-render</artifactId>
<version>${metaobjects.version}</version>
</dependency>
</dependencies>

For Spring integration: add metaobjects-core-spring.

Configure

<build>
<plugins>
<plugin>
<groupId>com.metaobjects</groupId>
<artifactId>metaobjects-maven-plugin</artifactId>
<version>${metaobjects.version}</version>
<executions>
<execution>
<id>generate</id>
<phase>generate-sources</phase>
<goals><goal>generate</goal></goals>
<configuration>
<loader>
<sourceDir>src/main/metaobjects</sourceDir>
</loader>
<generators>
<generator>
<classname>com.metaobjects.generator.spring.SpringDtoGenerator</classname>
<args>
<outputDir>${project.build.directory}/generated-sources/java</outputDir>
</args>
</generator>
<generator>
<classname>com.metaobjects.generator.spring.SpringControllerGenerator</classname>
<args>
<outputDir>${project.build.directory}/generated-sources/java</outputDir>
</args>
</generator>
<generator>
<classname>com.metaobjects.generator.spring.SpringRepositoryGenerator</classname>
<args>
<outputDir>${project.build.directory}/generated-sources/java</outputDir>
</args>
</generator>
</generators>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

Drop metadata under src/main/metaobjects/:

// src/main/metaobjects/meta.blog.json
{ "metadata.root": {
"package": "acme::blog",
"children": [
{ "object.entity": {
"name": "Author",
"children": [
{ "source.rdb": { "@table": "authors" } },
{ "field.long": { "name": "id" } },
{ "field.string": { "name": "name", "@required": true, "@maxLength": 200 } },
{ "field.string": { "name": "bio", "@maxLength": 2000 } },
{ "identity.primary": { "@fields": "id", "@generation": "increment" } }
]
}}
]
}}

Custom providers (optional)

Java uses SPI auto-discovery for type providers — drop your provider class on the classpath, list its FQCN in META-INF/services/com.metaobjects.registry.MetaDataTypeProvider, and MetaDataRegistry.getInstance() will compose it in dependency order alongside the core providers:

// src/main/java/com/example/providers/ExampleToolcallProvider.javapackagecom.example.providers;
importcom.metaobjects.registry.MetaDataTypeProvider;
importcom.metaobjects.registry.MetaDataRegistry;
publicclassExampleToolcallProviderimplementsMetaDataTypeProvider {
@OverridepublicStringgetProviderId() { return"example-template-toolcall"; }
@OverridepublicString[] getDependencies() { returnnewString[] { "core-types" }; }
@OverridepublicvoidregisterTypes(MetaDataRegistryregistry) {
// registry.register(...) — see the cross-port contract
}
}
# src/main/resources/META-INF/services/com.metaobjects.registry.MetaDataTypeProvider
com.example.providers.ExampleToolcallProvider

The provider contract is structurally identical to TS / C# / Python (id + dependencies + description + registerTypes body); the loader composes all providers via Kahn's algorithm and emits the same stable error codes on failure (ERR_PROVIDER_DUPLICATE_ID, _MISSING_DEPENDENCY, _DEPENDENCY_CYCLE).

For callers who want to bypass SPI auto-discovery — or compose extra consumer vocabulary on top of the full metamodel provider set so it still strict-loads against the spec contract (no --lax fallback) — the sanctioned seam is RegistryManifest.composeMetamodelRegistry(extraProviders), which composes the core metamodel providers plus extraProviders and runs the full spec-description + provenance-safe attr-scoping pipeline (hand the result to loader.setTypeRegistry(...)). Raw MetaDataRegistry.compose(...) composes only the explicit list, skips spec scoping, and is for internal/test partial sets. The cross-port contract lives in ../features/extending-with-providers.md.

Generate

mvn compile # runs the generate goal (bound to generate-sources)

Schema migrations are not a Java-port concern — author them with the TypeScript toolchain (@metaobjectsdev/cli migrate), then apply the resulting DDL to the database OMDB connects to. OMDB itself is pure data-access; the former runtime auto-create path was removed per ADR-0015.

Use

OMDB reads the same metadata at runtime and drives CRUD; no per-entity ORM boilerplate.

codegen-spring's only entity-shaped output is the immutable <Entity>Dto record — it generates no typed entity POJO. (A typed MetaObjectAware class is available separately, from JavaObjectCodeGenerator's flavored codegen — see Serializing generated objects below.) OMDB drives CRUD against the loaded metadata plus generic ValueObject instances, and its API is connection-first (you pass an ObjectConnection to each call):

importcom.metaobjects.loader.MetaDataLoader;
importcom.metaobjects.manager.ObjectConnection;
importcom.metaobjects.manager.QueryOptions;
importcom.metaobjects.manager.db.ObjectManagerDB;
importcom.metaobjects.manager.exp.Expression;
importcom.metaobjects.object.MetaObject;
importcom.metaobjects.object.value.ValueObject;
importjavax.sql.DataSource;
importjava.nio.file.Path;
importjava.util.Collection;
publicclassApp {
publicstaticvoidmain(String[] args) throwsException {
MetaDataLoaderloader = MetaDataLoader.fromDirectory(
"app", Path.of("src/main/metaobjects"));
DataSourceds = /* your javax.sql.DataSource */;
ObjectManagerDBom = newObjectManagerDB();
om.setDataSource(ds);
om.init();
MetaObjectauthor = loader.getMetaObjectByName("acme::blog::Author");
ObjectConnectionoc = om.getConnection();
try {
// CREATE — a generic ValueObject typed by the Author MetaObjectValueObjectrow = (ValueObject) author.newInstance();
row.setString("name", "Ada");
om.createObject(oc, row);
oc.commit();
// QUERY — all rows, or filtered via an ExpressionCollection<?> all = om.getObjects(oc, author, newQueryOptions());
ValueObjectmatch = (ValueObject) om.getObjects(
oc, author, newQueryOptions(newExpression("name", "Ada")))
.iterator().next();
// LOAD by primary key — re-reads the row into the objectom.loadObject(oc, match);
} finally {
om.releaseConnection(oc);
}
}
}

Spring wiring lives in metaobjects-core-spring; declare an ObjectManagerDB bean with the Spring DataSource and let Spring inject it into your services.

FR-004 — render engine

importcom.metaobjects.render.*;
importjava.nio.file.Path;
importjava.util.List;
importjava.util.Map;
Providerprovider = newFilesystemProvider(Path.of("./prompts"));
Map<String, Object> payload = Map.of(
"displayName", "Ada",
"postCount", 12L,
"posts", List.of(Map.of("title", "Hello")));
// RenderRequest is a record (template, ref, payload, provider, format, verify, maxChars);// pass a null template for a provider-resolved ref, and null verify/maxChars.// render() is an instance method.Stringout = newRenderer().render(
newRenderRequest(null, "lobby/welcome", payload, provider, "xml", null, null));

Verify.check(templateText, fields, options) returns a List<VerifyError> (empty = no drift) — it cross-checks a template's variables against its declared payload field tree (List<PayloadField>), flagging any variable absent from the payload (ERR_VAR_NOT_ON_PAYLOAD), unresolved partials, and unused required slots. Wire it into a Maven test (e.g. a JUnit assertion in the test phase).

Generators

GeneratorModuleOutput
SpringControllerGeneratormetaobjects-codegen-springOne <Entity>Controller.java per writable entity (source.rdb @kind="table"). Spring Boot 3.x / Spring Web MVC. Five CRUD endpoints (GET list / GET by id / POST / PATCH + PUT / DELETE) matching the cross-port REST API contract. ?sort, ?limit/?offset, ?withCount=1 envelope, 404 + 400 envelopes per the contract. Filter operators (eq/ne/gt/gte/lt/lte/in/like/isNull) ship via the generated <Entity>FilterAllowlist (SpringFilterAllowlistGenerator) + the runtime FilterParser, wired directly into the list handler.
SpringDtoGeneratormetaobjects-codegen-springOne <Entity>Dto.java per entity as a Java 21 record. Wrapped-primitive components (Long, Integer, Boolean) so missing JSON properties deserialise to null. Currency = Long (integer minor units cross-port invariant). Used as both request and response body.
SpringRepositoryGeneratormetaobjects-codegen-springOne <Entity>Repository.java per writable entity as a hand-stubbed Java interface the consumer implements with their preferred persistence layer (Spring Data JPA / jOOQ / plain JDBC — all out of MetaObjects' concern). Nests the SortClause record the controller calls into.
JavaObjectCodeGeneratormetaobjects-codegen-baseFlavor-selected via the flavor generator arg (com.metaobjects.generator.direct.object.javacode). flavor=pojoAware emits class <Name> extends PojoObject — a concrete MetaObjectAware class whose inherited getMetaData() back-reference breaks a default Jackson/Gson mapper (see Serializing generated objects below). flavor=valueObject emits a map-backed class <Name> extends ValueObject instead. Either flavor also emits a <Name>Extractor and a self-registering ObjectClassBindingProvider. For a plain default-Jackson-friendly type, use the codegen-spring record surface instead — never pojoAware.

Wire any of the three Spring generators via the Maven plugin's <generator> entry pointing at com.metaobjects.generator.spring.SpringControllerGenerator / SpringDtoGenerator / SpringRepositoryGenerator. The three are independently configurable; typical use is all three together (controller + DTO + repository).

Serializing generated objects

Two paths hand you a MetaObjectAware instance: the JavaObjectCodeGenerator flavored codegen above (a pojoAware or valueObject class), and the OMDB runtime (ObjectManagerDB.getObjects(...) / MetaObject.newInstance(), see Use above). Serialize either through the MetaObjects JSON layer (com.metaobjects.io.object.json) — JsonObjectWriter for the write side, JsonObjectReader for the read side — rather than a bare Jackson/Gson mapper:

importcom.metaobjects.io.object.json.JsonObjectWriter;
importcom.metaobjects.io.object.json.JsonObjectReader;
importcom.metaobjects.loader.MetaDataLoader;
importcom.metaobjects.object.MetaObject;
importjava.io.StringReader;
importjava.io.StringWriter;
importjava.nio.file.Path;
MetaDataLoaderloader = MetaDataLoader.fromDirectory("app", Path.of("src/main/metaobjects"));
MetaObjectmo = loader.getMetaObjectByName("acme::blog::Author");
// pojoAware-flavor generated class: public Author(MetaObject mo) { super(mo); }Authorauthor = newAuthor(mo);
author.setName("Ada");
StringWriterout = newStringWriter();
JsonObjectWriter.writeObject(author, out);
Stringjson = out.toString();
// {"@type":"acme::blog::Author","name":"Ada"}AuthorroundTripped = JsonObjectReader.readObject(Author.class, mo, newStringReader(json));

A default Jackson/Gson mapper pointed directly at a pojoAware-flavor class fails on the MetaObject back-reference every generated PojoObject subtype carries (the inherited getMetaData() getter leads a bean-style mapper into the metadata graph, and on a modular JVM into InaccessibleObjectException) — this is expected, not a bug to work around. If you want a type that serializes cleanly with a bare default mapper, generate the codegen-spring record surface instead (SpringDtoGenerator / SpringPayloadGenerator / SpringValueObjectGenerator) — never pojoAware.

Wire form (field.date / field.timestamp) — a Java rendering of the cross-port contract in normalization.md (the single source of truth):

FieldWire formExample
field.datecalendar date of the instant at UTC — YYYY-MM-DD"2026-06-03"
field.timestamp + @localTime: truewall clock of the instant at UTC, no Z"2026-06-03T14:30:00.123"
field.timestamp (default, tz-aware)UTC instant, with Z"2026-06-03T14:30:00.123Z"

The fraction is millisecond resolution, trailing zeros stripped, and the . plus fraction omitted entirely when zero (.123.123, .120.12, .100.1, .000→omitted). A null value writes JSON null. Readers stay tolerant and backward-compatible: a JSON number is still read as legacy epoch milliseconds; a JSON string is tried in order as an ISO instant (the Z form) → a local date-time (no Z) → a date-only form, and the error message names all three accepted forms if none match.

A hand-constructed field.date value carrying a sub-day time component writes as the calendar date only (truncated on first write, stable thereafter) — this matches the shipped OMDB DATE codec, which anchors DATE columns at midnight UTC.

Universal Angular 18 client

The browser-side Angular 18 client (@metaobjectsdev/angular + @metaobjectsdev/codegen-ts-angular, which live on the TypeScript side per the universal client recipesource-only, not published to npm) interoperates with the generated Spring controllers out of the box — the cross-port URL grammar and JSON wire shape are identical. Consumers wire EntityFetcherToken to a fetch wrapper that targets their Spring backend's apiPrefix (default /api); no Java-specific Angular code is needed.

CORS is the only typical hookup item: a Spring dev-server on port 8080 + an Angular dev-server on port 4200 will need @CrossOrigin on the generated controllers (or a global WebMvcConfigureraddCorsMappings(...) registration in the consumer's @Configuration). The generated controllers do not emit @CrossOrigin — adding it cross-port would require a CORS-policy configuration model that has not yet been specced.

Capability snapshot

FeatureStatus
Entities + fieldsYes
Relationships + FKYes (via OMDB)
Source kinds (table / view / storedProc)Yes
field.currency / field.enum / field.object + @storageYes
Templates + render (FR-004)Yes (metaobjects-render)
Payload-VO codegenYes — SpringPayloadGenerator (in metaobjects-codegen-spring) emits a Java 21 record per template, mirrors the Kotlin shape
Output parser codegen (FR-006)Yes — SpringOutputParserGenerator (in metaobjects-codegen-spring) — see usage below
MigrationsTS-only (@metaobjectsdev/cli migrate) — the Java migration engine and the OMDB runtime auto-create path were both removed (ADR-0015); apply the TS-produced DDL to the database
Drift verifyVerify.check / Verify.checkOutputPrompt (prompts). Live-DB schema-drift verification is part of the TS migration toolchain
Runtime metadataFull — OMDB ObjectManager
REST controller codegenSpring Web MVC — metaobjects-codegen-spring (FR-008 §2.1)

FR-006 — output parsing

SpringOutputParserGenerator (in metaobjects-codegen-spring) emits one <TemplateName>Parser Java class per template.output declaration — a Jackson-backed, throw-only parser around the @payloadRef payload record SpringPayloadGenerator already emits (no payload-shape re-declaration). Registered in the module's generator registry as output-parser.

// generated/NpcResponseParser.javapublicfinalclassNpcResponseParser {
privatestaticfinalObjectMapperMAPPER = newObjectMapper();
privateNpcResponseParser() {}
/** @throws JsonProcessingException on malformed JSON or a schema mismatch. */publicstaticNpcResponsePayloadparse(Stringtext) throwsJsonProcessingException {
returnMAPPER.readValue(text, NpcResponsePayload.class);
}
}

Consumer wiring:

StringllmResponse = myLlmClient.complete(promptText);
try {
NpcResponsePayloadnpc = NpcResponseParser.parse(llmResponse);
returnResponseEntity.ok(npc);
} catch (JsonProcessingExceptione) {
returnResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}

The same Verify API guards the output side: Verify.checkOutputPrompt(fragment, requiredFieldNames) checks the output-format prompt fragment names every required field, and Verify.check(...) (with output-tag slots supplied via its VerifyOptions) catches payload-VO ↔ parser drift at build time. Cross-port design is at ADR-0010; the feature reference is at features/templates-and-payloads.md. FR-010's tolerant extractLenient(loader, text) variant (returns an ExtractionResult<TPayload> instead of throwing) ships alongside parse().

Conformance status

Per-corpus pass counts move every release — see docs/CONFORMANCE.md for the current, authoritative per-port numbers (metamodel, YAML, render, verify, persistence, API contract). Java is green across all six active corpora today (Java doesn't run the persistence corpus's migration scenarios — those are TS-only, ADR-0015).

See also