diff --git a/docs/website/content/blog/build-time-codegen.md b/docs/website/content/blog/build-time-codegen.md new file mode 100644 index 0000000000..7ef2a1abbd --- /dev/null +++ b/docs/website/content/blog/build-time-codegen.md @@ -0,0 +1,374 @@ +--- +title: "OpenAPI, ORM, SVG and Lottie" +slug: build-time-codegen +url: /blog/build-time-codegen/ +date: '2026-06-01' +author: Shai Almog +description: An OpenAPI 3.x client generator that turns a spec into typed Codename One code, a JPA-shaped SQLite ORM, JAXB-shaped JSON / XML mappers, build-time SVG and Lottie transcoders, plus a declarative router and deep-link API. All ride on the same build-time codegen pipeline. +feed_html: 'OpenAPI, ORM, SVG and Lottie An OpenAPI client generator, a JPA-shaped SQLite ORM, JAXB-shaped JSON / XML mappers, build-time SVG / Lottie transcoders, and a declarative router with deep links. All on the same build-time codegen pipeline.' +--- + +![OpenAPI, ORM, SVG and Lottie](/blog/build-time-codegen.jpg) + +This is the third follow-up to [Friday's release post](/blog/metal-default-new-build-cloud-and-a-new-format/). Saturday's was about how you iterate; yesterday's was about new platform APIs in the core; today's is about a run of pieces that change how you write the structural parts of an app. + +The pieces are an OpenAPI client generator, a SQLite ORM, JSON and XML mappers, a component binder with validation, build-time SVG and Lottie transcoders, and a declarative router with deep links. All ride on a single **build-time codegen pipeline**: a Maven-plugin pass that reads annotations or declarative source files at build time and emits typed Java that compiles into your binary. No reflection, no service loader, no `Class.forName`. The "How it works" section at the end of this post covers the codegen plumbing once you have seen what it powers. + +## OpenAPI client generation + +The headline of this release for any team that talks to a backend. + +A new `cn1:generate-openapi-client` Mojo reads an OpenAPI 3.x JSON spec (a URL or a local file) and writes typed Codename One client code that compiles into your app: + +- One `@Mapped` POJO per `components.schemas` entry. +- One `Api.java` class per OpenAPI tag, with one fluent method per operation. +- Every method routes through `Rest.` + `Mappers.toJson` + `fetchAsMapped` / `fetchAsMappedList`, so the generated surface integrates with the rest of the framework instead of dragging in a separate HTTP stack. + +Wire it into the project's `pom.xml`: + +```xml + + com.codenameone + codenameone-maven-plugin + + + petstore-client + generate-openapi-client + + https://petstore3.swagger.io/api/v3/openapi.json + com.example.petstore + + + + +``` + +`mvn generate-sources` picks the spec up, downloads it, and writes one file per schema and one per tag under `target/generated-sources/`. The Petstore reference spec exercised end-to-end produces six model classes (`Pet`, `Order`, `Customer`, `Tag`, `Category`, `User`) and three Api classes (`PetApi`, `StoreApi`, `UserApi`), and the nine generated `.class` files compile cleanly against `codenameone-core`. Documented at [the OpenAPI codegen Maven goal](https://www.codenameone.com/developer-guide/#_appendix_goal_generate_openapi_client). + +In application code you call the generated `Api` class the same way you would call any other Java method: + +```java +PetApi pets = new PetApi(); + +// Returns AsyncResource; resolves with the deserialised object. +pets.getPetById(42).onResult((pet, err) -> { + if (err == null) Log.p("Got " + pet.getName()); +}); + +// Returns AsyncResource>. +pets.findPetsByStatus("available").onResult((list, err) -> { + if (err == null) { + for (Pet p : list) Log.p(p.getName()); + } +}); + +// POST with a request body. addPet takes a Pet, returns a Pet. +Pet candidate = new Pet(); +candidate.setName("Mittens"); +candidate.setStatus("available"); +pets.addPet(candidate).onResult((created, err) -> { /* ... */ }); +``` + +There is no hand-rolled `ConnectionRequest` setup, no manual JSON parsing, no string-typed request bodies. The generated client takes a typed `Pet`, serialises it with `Mappers.toJson(...)`, fires the right HTTP verb, deserialises the response with `Mappers.fromJson(...)`, and surfaces the result through the framework's `AsyncResource` so your callback fires on the EDT. + +For teams who already publish an OpenAPI spec as part of their backend (most modern backend frameworks do this automatically; FastAPI, Spring's `springdoc-openapi`, NestJS, ASP.NET Core, Go's `gnostic`), the practical effect is that the mobile client's bindings stay in sync with the backend without anyone hand-writing a single network call. Update the spec, re-run `mvn generate-sources`, and the new and changed endpoints land in your app as typed Java the IDE picks up immediately. + +It is the kind of change that is most useful when you do not know you have it: pull a fresh spec, rebuild, and your IDE highlights every place in the codebase that called a renamed endpoint or passed the wrong type to a parameter. + +## SQLite ORM + +`@Entity` marks the class; `@Id` and `@Column` shape the schema; `@DbTransient` opts a field out: + +```java +@Entity +public class TodoItem { + @Id @Column long id; + @Column String title; + @Column(name = "completed_at") + Date completedAt; + @DbTransient Object cachedView; +} + +Dao dao = EntityManager.open("todos.db").dao(TodoItem.class); +dao.createTable(); +dao.insert(new TodoItem(0, "Read the post", null)); + +List open = dao.find("completed_at IS NULL", new Object[] {}); +TodoItem byId = dao.findById(42); +dao.delete(byId); +``` + +The generated DAO does the typed work underneath. No reflection in `insert`; the generated code calls `setString(1, e.title)` and `setLong(2, e.id)` directly against the SQLite `PreparedStatement`. Validation at build time catches missing `@Id`, fields that look like relationships but are not yet supported, and abstract entity classes; the build fails with a class name and a reason. + +**For JPA / Hibernate developers,** the API is intentionally familiar. `@Entity`, `@Id`, `@Column`, and `@Transient` (here renamed `@DbTransient` to avoid colliding with `java.beans.Transient`) carry the same meaning they do under `javax.persistence` / `jakarta.persistence`. The `EntityManager` name is the same. `Dao#findById`, `Dao#findAll`, `Dao#find(where, params)`, `Dao#insert`, `Dao#update`, `Dao#delete` line up with the basic JPA repository contract. The query language is plain SQL (there is no JPQL or Criteria DSL) but the annotation surface, the lifecycle, and the runtime methods will feel like a long-lost friend to anyone with server-side Java persistence experience. + +## JSON / XML mapping + +`@Mapped` marks a class as a transferable POJO. `@JsonProperty` and `@XmlElement` (plus `@XmlRoot`, `@XmlAttribute`, `@JsonIgnore`, `@XmlTransient`) shape the wire format. The runtime entry points are `Mappers.toJson(...)`, `Mappers.fromJson(...)`, `Mappers.toXml(...)`, `Mappers.fromXml(...)`: + +```java +@Mapped +public class User { + @JsonProperty("user_id") long id; + @JsonProperty String name; + @JsonProperty("created_at") + Date createdAt; + @JsonIgnore String passwordHash; +} + +String json = Mappers.toJson(user); +User back = Mappers.fromJson(json, User.class); +``` + +The same `@Mapped` POJO is the type the typed `Rest` helpers accept: + +```java +Rest.get("https://api.example.com/users/42") + .fetchAsMapped(User.class) + .onResult((user, err) -> { /* ... */ }); + +Rest.get("https://api.example.com/users") + .fetchAsMappedList(User.class) + .onResult((users, err) -> { /* ... */ }); +``` + +`Rest.fetchAsJsonList` (top-level JSON arrays, no `{"root":[...]}` envelope trick), `JSONWriter` (the complement of `JSONParser`, with fluent builders and streaming variants for `Writer` and `OutputStream`), and `URLImage.setDefaultBearerToken` (auth headers on image fetches) all ship alongside. + +**For JAXB developers,** the XML surface (`@XmlRoot`, `@XmlElement`, `@XmlAttribute`, `@XmlTransient`) is a direct port of the long-established `javax.xml.bind.annotation` surface. The same model class can be both `@XmlRoot`-decorated and `@JsonProperty`-decorated, which gives you a single source of truth for both wire formats. The JSON surface adopts the Jackson convention (`@JsonProperty`, `@JsonIgnore`) that nearly every modern JVM JSON binding (Jackson, Moshi, kotlinx-serialization) inherited. + +## Component binding with validation + +The fourth annotation processor on the same pipeline is the component binder. `@Bindable` marks a model class; `@Bind(name = "userField")` ties a field to a component on a form by the component's `name`. Field-level validation annotations compose with `@Bind` on the same field: + +```java +@Bindable +public class SignupModel { + @Bind(name = "userField") @Required @Length(min = 3) + private String user; + + @Bind(name = "emailField") @Required @Email + private String email; + + @Bind(name = "ageField") @Numeric(min = 13, max = 120) + private String age; + + @Bind(name = "roleField") @ExistIn({ "admin", "editor", "viewer" }) + private String role; +} +``` + +The matching form sets a `name` on each component so the binder can find them: + +```java +TextField user = new TextField(); user.setName("userField"); +TextField email = new TextField(); email.setName("emailField"); +TextField age = new TextField(); age.setName("ageField"); +ComboBox role = new ComboBox<>("admin", "editor", "viewer"); +role.setName("roleField"); + +Button submit = new Button("Sign up"); + +Form form = new Form("Sign Up", BoxLayout.y()); +form.add(user).add(email).add(age).add(role).add(submit); +form.show(); + +SignupModel model = new SignupModel(); +Binding binding = Binders.bind(model, form); +binding.getValidator().addSubmitButtons(submit); +``` + +`Binding` is the handle: `refresh()` re-reads the model into the components, `commit()` writes the components back, `disconnect()` tears the listeners down. Multiple validation annotations on a single field compose via `Validator.addConstraint(Component, Constraint...)` and `GroupConstraint` (first failure wins). `@Validate(MyClass.class)` is the escape hatch for hand-written `Constraint` implementations. The validation set: `@Required`, `@Length`, `@Regex`, `@Email`, `@Url`, `@Numeric`, `@ExistIn`, `@Validate`. + +The new `BindAttr` enum lets `@Bind` target a specific attribute of the component (`TEXT`, `UIID`, `SELECTED`, ...) when the default ("write a `String` field into the component's text") is not what you want. + +## SVG at build time + +Drop an SVG into `src/main/css/`, alongside `theme.css`: + +``` +src/main/css/ + theme.css + star.svg + gradient_circle.svg + path_arrow.svg + rounded_button.svg + wave.svg + pro_badge.svg + clipped_badge.svg +``` + +After the next build, every SVG is a regular Codename One `Image`. **An SVG handled by the transcoder is a vector image, but it is still an `Image`.** Everywhere a raster `Image` works (`Label.setIcon`, `Button.setIcon`, `BorderLayout.NORTH`, the toolbar, a `MultiButton`'s leading icon, a CSS `background: url(...)` rule), the SVG works too. The difference is that it stays crisp at any size: the same source file is sharp at a 16-point list-row icon, a 64-point hero header, and a 256-point launch screen, on every DPI bucket. + +A grid of the static SVGs from the hellocodenameone fixture, rendered through the new pipeline: + +![Static SVGs rendered by the build-time transcoder on iOS Metal: filled star, gradient-filled circle, path arrow, rounded button, two stroked wave paths, gradient-filled PRO badge, clipped badge](/blog/build-time-codegen/svg-static.png) + +### Sizing in millimeters + +The SVG transcoder's most useful feature is also the one most easily missed: **size every SVG in millimeters from CSS**. SVGs in the wild routinely declare odd `width` / `height` attributes (a 1024×1024 export of a 24×24 icon, no dimensions at all, design-pixel values from one specific framework). Pinning the rendered size in millimeters sidesteps all of that. + +```css +HomeIcon { + background: url(home.svg); + cn1-svg-width: 6mm; + cn1-svg-height: 6mm; + bg-type: image_scaled_fit; +} + +LogoBanner { + background: url(logo.svg); + cn1-svg-width: 32mm; + cn1-svg-height: 12mm; +} +``` + +A 6 mm icon is 6 mm tall on a 1× desktop, 6 mm on a high-DPI handset, and 6 mm on a 4K tablet. The transcoder routes both values through `Display.convertToPixels()` at install time, the same way `font-size: 3mm` already behaves elsewhere in Codename One CSS. No design-pixel guesswork, no DPI bucket to choose, no scaling surprise when the artist re-exports the source SVG at a different resolution. + +If a project does not use CSS for theming, the two-`float` constructor on the generated class takes millimeters directly: `new com.codename1.generated.svg.Home(6f, 6f)`. + +### Coverage and what we still want feedback on + +The transcoder is a `maven/svg-transcoder/` module that parses SVG with `javax.xml` StAX. No Batik, no Flamingo, no external dependencies. Coverage targets what real-world icon SVGs use: `rect` (rounded corners included), `circle`, `ellipse`, `line`, `polyline`, `polygon`, the full `path` grammar (`M` / `L` / `H` / `V` / `C` / `S` / `Q` / `T` / `A` / `Z` plus relative-coordinate and smooth-curve reflection), groups with affine transforms (`translate`, `scale`, `rotate`, `skew`, `matrix`), linear gradients via `LinearGradientPaint`, fill, stroke, stroke-width, linecap, linejoin, opacity. + +SMIL animations are supported in the same pipeline: ``, `` (`translate`, `scale`, `rotate`), and ``. Time values interpolate against wall-clock time on every paint, with `from` / `to` / `values` / `begin` / `dur` / `repeatCount` / `fill="freeze"` honoured. + +Text and clip-path landed in the [follow-up PR for the static SVG fixtures](https://github.com/codenameone/CodenameOne/pull/5056), and both are visible in the screenshot above (the "Codename One / build-time SVG" wordmark in the rounded button, the "PRO" badge text, and the clip-path-shaped rounded-corner badge underneath). `` and `` work with single-style fills and transforms; `` referenced via `clip-path="url(#id)"` works against `rect`, `circle`, and `path` clip shapes (nested clip refs are ignored). + +What is still not supported: SVG `filter` primitives, `` (treated as a clip, so alpha masking falls back to opaque), `` (falls back to the first-stop colour), and CSS-in-SVG (style rules inside the SVG document; the transcoder reads presentation attributes and the inline `style="..."` attribute, but a `