Skip to content

Commit 49c6f51

Browse files
Copilotsimbo1905
andauthored
Add offline jdt2jar CLI and distroless packaging (#148)
* Initial plan * Issue #0 add jdt2jar CLI and container Agent-Logs-Url: https://github.com/simbo1905/java.util.json.Java21/sessions/46541410-0395-47ca-bf41-41ba6d0a8dc6 Co-authored-by: simbo1905 <322608+simbo1905@users.noreply.github.com> * Issue #0 refine jdt2jar packaging Agent-Logs-Url: https://github.com/simbo1905/java.util.json.Java21/sessions/46541410-0395-47ca-bf41-41ba6d0a8dc6 Co-authored-by: simbo1905 <322608+simbo1905@users.noreply.github.com> * Issue #0 address jdt2jar review feedback Agent-Logs-Url: https://github.com/simbo1905/java.util.json.Java21/sessions/46541410-0395-47ca-bf41-41ba6d0a8dc6 Co-authored-by: simbo1905 <322608+simbo1905@users.noreply.github.com> * Issue #148 scope jdt2jar Dockerfile to submodule, add GHCR publish and CI Docker builds - Move Dockerfile from repo root to jdt2jar/ (built with -f jdt2jar/Dockerfile .) - Add jdt2jar/.dockerignore to exclude build artifacts - Rewrite jdt2jar/README.md with full Docker/GHCR usage instructions - Update root README.md to signpost to jdt2jar submodule for optional tool - Add docker-build job to ci.yml (Java 25, smoke test) - Add docker-build job to maven.yml (Java 25, smoke test after tests pass) - Add docker-publish job to release-on-tag.yml (pushes version + latest to ghcr.io) - Release notes now include Docker image pull commands How to verify: docker build -t jdt2jar -f jdt2jar/Dockerfile . docker run --rm jdt2jar --help * Issue #148 fix expected test count from 1354 to 1356 for jdt2jar module tests * Issue #148 clarify two JTD validation paths in READMEs - Root README: module table distinguishes interpreter (infrequent config) vs codegen (hot-path) - json-java21-jtd/README: documents config-parsing use case, links to codegen - json-java21-jtd-codegen/README: documents hot-path use case, links to interpreter - jdt2jar/README: documents build-time pre-compilation bridging both paths - All three sub-READMEs note JDK incubator future direction * Issue #148 separate interpreter and codegen JtdValidator interfaces - Interpreter module (json-java21-jtd): JtdValidator.compileInterpreter() replaces deprecated compile() - Codegen module (json-java21-jtd-codegen): new JtdValidator.compileGenerated() in own package - Generated bytecode implements json.java21.jtd.codegen.JtdValidator (not interpreter's interface) - ValidatorMain uses codegen's JtdValidator for generated validators - Jdt2Jar bundles codegen's JtdValidator.class in output JARs - All tests updated to use new method names - pom.xml: -Werror + showDeprecation across all modules - READMEs updated with clear two-path guidance How to verify: jenv local 21 && ./mvnw test -pl json-java21-jtd -am jenv local 25 && ./mvnw test -pl json-java21-jtd,json-java21-jtd-codegen -am * Issue #148 fix expected test count from 1356 to 1355 (removed compileGenerated test from interpreter module) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simbo1905 <322608+simbo1905@users.noreply.github.com>
1 parent d3fb609 commit 49c6f51

26 files changed

Lines changed: 1277 additions & 159 deletions

File tree

.github/workflows/ci.yml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,40 @@ jobs:
3939
for k in totals: totals[k]+=int(r.get(k,'0'))
4040
except Exception:
4141
pass
42-
exp_tests=1354
42+
exp_tests=1355
4343
exp_skipped=0
4444
if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped:
4545
print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}")
4646
sys.exit(1)
4747
print(f"OK totals: {totals}")
4848
PY
49+
50+
docker-build:
51+
name: Build Docker image (Java 25)
52+
runs-on: ubuntu-latest
53+
steps:
54+
- name: Checkout
55+
uses: actions/checkout@v4
56+
57+
- name: Set up JDK 25
58+
uses: actions/setup-java@v4
59+
with:
60+
distribution: oracle
61+
java-version: '25'
62+
cache: 'maven'
63+
64+
- name: Set up Docker Buildx
65+
uses: docker/setup-buildx-action@v3
66+
67+
- name: Build Docker image
68+
uses: docker/build-push-action@v6
69+
with:
70+
context: .
71+
file: jdt2jar/Dockerfile
72+
push: false
73+
load: true
74+
tags: jdt2jar:ci
75+
76+
- name: Smoke test
77+
run: |
78+
docker run --rm jdt2jar:ci --help

.github/workflows/maven.yml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,35 @@ jobs:
5252
restore-keys: ${{ runner.os }}-m2-java25
5353

5454
- name: Test full project (Java 25)
55-
run: mvn clean test
55+
run: mvn clean test
56+
57+
docker-build:
58+
name: Build Docker image (Java 25)
59+
runs-on: ubuntu-latest
60+
needs: test-java25
61+
62+
steps:
63+
- uses: actions/checkout@v4
64+
65+
- name: Set up JDK 25
66+
uses: actions/setup-java@v4
67+
with:
68+
java-version: '25'
69+
distribution: 'oracle'
70+
cache: 'maven'
71+
72+
- name: Set up Docker Buildx
73+
uses: docker/setup-buildx-action@v3
74+
75+
- name: Build Docker image
76+
uses: docker/build-push-action@v6
77+
with:
78+
context: .
79+
file: jdt2jar/Dockerfile
80+
push: false
81+
load: true
82+
tags: jdt2jar:ci
83+
84+
- name: Smoke test
85+
run: |
86+
docker run --rm jdt2jar:ci --help

.github/workflows/release-on-tag.yml

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ on:
66
- 'release/[0-9]*.[0-9]*.[0-9]*'
77

88
permissions:
9-
contents: write # push tags, push commits
9+
contents: write
1010
pull-requests: write
11+
packages: write
1112

1213
concurrency:
1314
group: release-${{ github.ref }}
@@ -34,11 +35,26 @@ jobs:
3435
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
3536
gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}
3637

38+
- name: Extract version from tag
39+
id: version
40+
run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
41+
3742
- name: Create GitHub Release with notes
3843
uses: softprops/action-gh-release@v2
3944
with:
4045
tag_name: ${{ github.ref_name }}
4146
generate_release_notes: true
47+
body: |
48+
## Docker Image
49+
50+
Pre-built distroless container image available on GitHub Container Registry:
51+
52+
```bash
53+
docker pull ghcr.io/${{ github.repository_owner }}/java.util.json.java21/jdt2jar:${{ steps.version.outputs.version }}
54+
docker pull ghcr.io/${{ github.repository_owner }}/java.util.json.java21/jdt2jar:latest
55+
```
56+
57+
See [jdt2jar/README.md](https://github.com/${{ github.repository }}/blob/main/jdt2jar/README.md) for usage.
4258
4359
- name: Build and Deploy to Central (release profile)
4460
env:
@@ -76,3 +92,42 @@ jobs:
7692
--base main \
7793
--head "${{ steps.prbranch.outputs.branch }}" \
7894
|| echo "PR already exists or nothing to compare"
95+
96+
docker-publish:
97+
name: Publish Docker image to GHCR
98+
runs-on: ubuntu-latest
99+
needs: release
100+
steps:
101+
- name: Checkout
102+
uses: actions/checkout@v4
103+
104+
- name: Set up JDK 25
105+
uses: actions/setup-java@v4
106+
with:
107+
distribution: oracle
108+
java-version: '25'
109+
cache: 'maven'
110+
111+
- name: Set up Docker Buildx
112+
uses: docker/setup-buildx-action@v3
113+
114+
- name: Log in to GHCR
115+
uses: docker/login-action@v3
116+
with:
117+
registry: ghcr.io
118+
username: ${{ github.actor }}
119+
password: ${{ secrets.GITHUB_TOKEN }}
120+
121+
- name: Extract version from tag
122+
id: version
123+
run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
124+
125+
- name: Build and push Docker image
126+
uses: docker/build-push-action@v6
127+
with:
128+
context: .
129+
file: jdt2jar/Dockerfile
130+
push: true
131+
tags: |
132+
ghcr.io/${{ github.repository_owner }}/java.util.json.java21/jdt2jar:${{ steps.version.outputs.version }}
133+
ghcr.io/${{ github.repository_owner }}/java.util.json.java21/jdt2jar:latest

README.md

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ This repo is organized into the following modules:
1414
| Module | What it is | JDK |
1515
| --- | --- | --- |
1616
| `json-java21` | Core `java.util.json` backport (parser, immutable types, `Json` API) | 21+ |
17-
| `json-java21-jtd` | JSON Type Definition (JTD) validator implementing RFC 8927, with stack-machine interpreter and optional bytecode codegen interface (`JtdValidator`) | 21+ |
18-
| `json-java21-jtd-codegen` | Bytecode code generator for JTD schemas using JDK 24+ ClassFile API (JEP 484); generates Java 21-compatible `.class` files for ~9x faster validation | 24+ (auto-skipped on JDK 21) |
17+
| `json-java21-jtd` | JTD (RFC 8927) stack-machine interpreter — ideal for infrequent config parsing and one-time validation | 21+ |
18+
| `json-java21-jtd-codegen` | Bytecode code generator for JTD schemas — ahead-of-time compiled validators for repeated hot-path validation | 24+ (auto-skipped on JDK 21) |
19+
| `jdt2jar` | CLI + distroless container to pre-compile JTD schemas into standalone validator JARs (eliminates JDK 24+ runtime requirement) | 24+ (auto-skipped on JDK 21) |
1920
| `json-java21-jsonpath` | JsonPath query engine over `jdk.sandbox.java.util.json` values (Goessner-style: filters, slices, recursive descent, unions) | 21+ |
2021
| `json-compatibility-suite` | JSON Test Suite conformance reporter (tests against [nst/JSONTestSuite](https://github.com/nst/JSONTestSuite)) | 21+ |
2122
| `json-java21-api-tracker` | Daily upstream API drift detector — fetches OpenJDK sandbox sources, compares public API signatures, reports differences | 25+ |
@@ -335,9 +336,12 @@ Such vulnerabilities existed at one point in the upstream OpenJDK sandbox implem
335336

336337
## JSON Type Definition (JTD) Validator
337338

338-
This repo contains an incubating JTD validator that has the core JSON API as its only dependency. This sub-project demonstrates how to build realistic JSON heavy logic using the API. It follows Data Oriented Programming principles: it compiles JTD schemas into an immutable structure of records. For validation it parses the JSON document to the generic structure and uses the thread-safe parsed schema and a stack to visit and validate the parsed JSON.
339+
This repo includes two JTD validation paths for different use cases:
339340

340-
A complete JSON Type Definition validator is included (module: json-java21-jtd).
341+
- **Interpreter** ([`json-java21-jtd`](json-java21-jtd/README.md)) — stack-machine validator for infrequent config parsing and one-time validation. Runs on JDK 21+ with zero extra dependencies.
342+
- **Bytecode codegen** ([`json-java21-jtd-codegen`](json-java21-jtd-codegen/README.md)) — generates dedicated validator classes for repeated hot-path validation (~9x faster). Requires JDK 24+ at build time; generated classes run on JDK 21+.
343+
344+
> `java.util.json` has entered the JDK incubator (`jdk.incubator.json`). Once the API stabilises in the JDK itself, generated bytecode validators can depend directly on future JDK classes with zero library overhead.
341345
342346
### Empty Schema `{}` Semantics (RFC 8927)
343347

@@ -349,43 +353,19 @@ Per **RFC 8927 (JSON Typedef)**, the empty schema `{}` is the **empty form** and
349353
> `empty = {}`
350354
> **Empty form:** A schema in the empty form accepts all JSON values and produces no errors.
351355
352-
⚠️ Note: Some tools or in-house validators mistakenly interpret `{}` as "object with no
353-
properties allowed." **That is not JTD.** This implementation follows RFC 8927 strictly.
354-
355356
```java
356357
import json.java21.jtd.Jtd;
357358
import jdk.sandbox.java.util.json.*;
358359

359-
// Compile JTD schema
360-
JsonValue schema = Json.parse("""
361-
{
362-
"properties": {
363-
"name": {"type": "string"},
364-
"age": {"type": "int32"}
365-
}
366-
}
367-
""");
368-
369-
// Validate JSON
370-
JsonValue data = Json.parse("{\"name\":\"Alice\",\"age\":30}");
360+
JsonValue schema = Json.parse("{\"properties\":{\"name\":{\"type\":\"string\"}}}");
361+
JsonValue data = Json.parse("{\"name\":\"Alice\"}");
371362
Jtd validator = new Jtd();
372363
Jtd.Result result = validator.validate(schema, data);
373364
// result.isValid() => true
374365
```
375366

376367
### JTD RFC 8927 Compliance
377368

378-
The validator provides full RFC 8927 compliance with comprehensive test coverage:
379-
380-
```bash
381-
# Run all JTD compliance tests
382-
./mvnw test -pl json-java21-jtd -Dtest=JtdSpecIT
383-
384-
# Run with detailed logging
385-
./mvnw test -pl json-java21-jtd -Djava.util.logging.ConsoleHandler.level=FINE
386-
```
387-
388-
Features:
389369
- ✅ Eight mutually-exclusive schema forms (RFC 8927 §2.2)
390370
- ✅ Standardized error format with instance and schema paths
391371
- ✅ Primitive type validation with proper ranges
@@ -394,6 +374,12 @@ Features:
394374
- ✅ Discriminator tag exemption from additional properties
395375
- ✅ Stack-based validation preventing StackOverflowError
396376

377+
## JTD to JAR Compiler (Optional)
378+
379+
An optional `jdt2jar` CLI tool and distroless Docker image are available for pre-compiling JTD schemas into standalone validator JARs at build time. This eliminates the JDK 24+ runtime requirement for generated validators — the JARs run on JDK 21+.
380+
381+
See [`jdt2jar/README.md`](jdt2jar/README.md) for build instructions, container usage, and the pre-built image on GitHub Container Registry (`ghcr.io`).
382+
397383
## Building
398384

399385
Requires JDK 21 or later. Build with Maven:

jdt2jar/.dockerignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Build outputs
2+
**/target/
3+
4+
# IDE
5+
.idea/
6+
*.iml
7+
.vscode/
8+
.eclipse/
9+
10+
# OS
11+
.DS_Store
12+
13+
# Git
14+
.git/
15+
.gitignore
16+
17+
# Env
18+
.env
19+
20+
# Local test artifacts
21+
/tmp/jdt2jar-*/

jdt2jar/Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# syntax=docker/dockerfile:1
2+
3+
FROM eclipse-temurin:24-jdk AS build
4+
WORKDIR /build
5+
COPY . .
6+
RUN ["./mvnw", "-pl", "jdt2jar", "-am", "package", "-DskipTests", "-Dsurefire.failIfNoSpecifiedTests=false"]
7+
RUN ["java", "-cp", "/build/jdt2jar/target/jdt2jar.jar", "json.java21.jdt2jar.build.DockerImageBuilder", "/build/jdt2jar/target/jdt2jar.jar", "/opt/jre"]
8+
RUN ["mkdir", "-p", "/empty-work/tmp", "/empty-app"]
9+
10+
FROM gcr.io/distroless/base-debian13:nonroot
11+
COPY --from=build --chown=65532:65532 /empty-work /work
12+
COPY --from=build --chown=65532:65532 /empty-app /app
13+
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.io.tmpdir=/work/tmp -XX:+ExitOnOutOfMemoryError"
14+
COPY --from=build /opt/jre /jre
15+
COPY --from=build /build/jdt2jar/target/jdt2jar.jar /app/jdt2jar.jar
16+
ENTRYPOINT ["/jre/bin/java","-jar","/app/jdt2jar.jar"]

jdt2jar/README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# jdt2jar
2+
3+
`jdt2jar` compiles a JTD schema into a standalone validator JAR at build time. The generated JAR runs on JDK 21+ with no JDK 24+ runtime dependency.
4+
5+
## Use Case
6+
7+
This tool bridges the gap between the interpreter and codegen paths:
8+
9+
- **Interpreter** ([`json-java21-jtd`](../json-java21-jtd/README.md)): ideal for infrequent config parsing — simple, no build step, runs on JDK 21+.
10+
- **Codegen** ([`json-java21-jtd-codegen`](../json-java21-jtd-codegen/README.md)): ideal for repeated hot-path validation — ~9x faster, but requires JDK 24+ at runtime.
11+
- **jdt2jar**: pre-compiles schemas into validator JARs at build time (using JDK 24+), then deploys them to any JDK 21+ runtime. Best for CI/CD pipelines, distroless containers, or environments where you want JIT-optimised validators without shipping a JDK 24+ runtime.
12+
13+
> **Future note**: `java.util.json` has entered the JDK incubator (`jdk.incubator.json`). Once the API stabilises in the JDK itself, generated bytecode validators can depend directly on future JDK classes rather than this backport, making them even more efficient with zero library overhead.
14+
15+
## CLI
16+
17+
```bash
18+
jdt2jar <schema.json> [options]
19+
```
20+
21+
Options:
22+
23+
- `--output <path>`: output JAR path (default: `<schema-name>-validator.jar`)
24+
- `--package <name>`: generated package name (default: `jtd.generated`)
25+
- `--class <name>`: validator class name (default: `SchemaValidator`)
26+
- `--main`: include a standalone `java -jar` entry point
27+
- `--runtime <version>`: target bytecode version (default: 21)
28+
- `--include-sources`: write a companion `.java` file next to the JAR
29+
- `--help`: show usage
30+
31+
## Container Image
32+
33+
A minimal distroless container image is available for offline schema compilation without a full JDK.
34+
35+
### Pre-built Image (GitHub Container Registry)
36+
37+
```bash
38+
# Pull the latest image
39+
docker pull ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest
40+
41+
# Pull a specific release version
42+
docker pull ghcr.io/simbo1905/java.util.json.java21/jdt2jar:2026.02.05
43+
```
44+
45+
### Build Locally
46+
47+
Requires Docker and JDK 24+ (for the build stage). Build from the repository root:
48+
49+
```bash
50+
docker build -t jdt2jar -f jdt2jar/Dockerfile .
51+
```
52+
53+
### Usage
54+
55+
```bash
56+
# Show help
57+
docker run --rm ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest --help
58+
59+
# Compile a schema to a validator JAR (using docker cp for file I/O)
60+
cid=$(docker create --name jdt2jar-build ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest /work/person.jtd.json --output /work/person-validator.jar --main)
61+
docker cp person.jtd.json jdt2jar-build:/work/person.jtd.json
62+
docker start -a jdt2jar-build
63+
docker cp jdt2jar-build:/work/person-validator.jar .
64+
docker rm jdt2jar-build
65+
66+
# Validate a payload with the generated JAR
67+
java -jar person-validator.jar --validate payload.json
68+
# Or validate inside a container
69+
cid=$(docker create --name jdt2jar-validate --entrypoint /jre/bin/java ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest -jar /work/person-validator.jar --validate /work/payload.json)
70+
docker cp person-validator.jar jdt2jar-validate:/work/person-validator.jar
71+
docker cp payload.json jdt2jar-validate:/work/payload.json
72+
docker start -a jdt2jar-validate
73+
docker rm jdt2jar-validate
74+
```
75+
76+
### Image Properties
77+
78+
- **Base**: `gcr.io/distroless/base-debian13:nonroot`
79+
- **Runtime**: jlink-minimized JDK 24 (~40 MB)
80+
- **Total size**: ~111 MB disk / ~31 MB content
81+
- **User**: `nonroot` (uid 65532)
82+
- **Shell**: none (distroless)
83+
- **Writable directories**: `/work` (for schema input and JAR output)
84+
85+
### Security Scanning
86+
87+
```bash
88+
syft packages image:ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest
89+
grype ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest
90+
```

0 commit comments

Comments
 (0)