();
+ var matchingCount = 0;
+ var missingUpstream = 0;
+ var differentApi = 0;
+
+ for (final var clazz : localClasses) {
+ final var localApi = extractLocalApi(clazz);
+ final var upstreamSource = upstreamSources.get(clazz.getName());
+ final var upstreamApi = extractUpstreamApi(upstreamSource, clazz.getName());
+
+ final var diff = compareApis(localApi, upstreamApi);
+ differences.add(diff);
+
+ // Count statistics
+ final var status = ((JsonString) diff.members().get("status")).value();
+ switch (status) {
+ case "MATCHING" -> matchingCount++;
+ case "UPSTREAM_ERROR" -> missingUpstream++;
+ case "DIFFERENT" -> differentApi++;
+ }
+ }
+
+ // Build summary
+ final var summary = JsonObject.of(Map.of(
+ "totalClasses", JsonNumber.of(localClasses.size()),
+ "matchingClasses", JsonNumber.of(matchingCount),
+ "missingUpstream", JsonNumber.of(missingUpstream),
+ "differentApi", JsonNumber.of(differentApi)
+ ));
+
+ reportMap.put("summary", summary);
+ reportMap.put("differences", JsonArray.of(differences));
+
+ final var duration = Duration.between(startTime, Instant.now());
+ reportMap.put("durationMs", JsonNumber.of(duration.toMillis()));
+
+ LOGGER.info("Comparison completed in " + duration.toMillis() + "ms");
+
+ return JsonObject.of(reportMap);
+ }
+}
\ No newline at end of file
diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java
new file mode 100644
index 0000000..49f1427
--- /dev/null
+++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java
@@ -0,0 +1,51 @@
+package io.github.simbo1905.tracker;
+
+import jdk.sandbox.java.util.json.Json;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/// Command-line runner for the API Tracker
+///
+/// Usage: java io.github.simbo1905.tracker.ApiTrackerRunner [loglevel]
+/// where loglevel is one of: SEVERE, WARNING, INFO, FINE, FINER, FINEST
+public class ApiTrackerRunner {
+
+ public static void main(String[] args) {
+ // Configure logging based on command line argument
+ final var logLevel = args.length > 0 ? Level.parse(args[0].toUpperCase()) : Level.INFO;
+ configureLogging(logLevel);
+
+ System.out.println("=== JSON API Tracker ===");
+ System.out.println("Comparing local jdk.sandbox.java.util.json with upstream java.util.json");
+ System.out.println("Log level: " + logLevel);
+ System.out.println();
+
+ try {
+ // Run the full comparison
+ final var report = ApiTracker.runFullComparison();
+
+ // Pretty print the report
+ System.out.println("=== Comparison Report ===");
+ System.out.println(Json.toDisplayString(report, 2));
+
+ } catch (Exception e) {
+ System.err.println("Error during comparison: " + e.getMessage());
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void configureLogging(Level level) {
+ // Get root logger
+ final var rootLogger = Logger.getLogger("");
+ rootLogger.setLevel(level);
+
+ // Configure console handler
+ for (var handler : rootLogger.getHandlers()) {
+ if (handler instanceof ConsoleHandler) {
+ handler.setLevel(level);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java
new file mode 100644
index 0000000..0a2d853
--- /dev/null
+++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java
@@ -0,0 +1,230 @@
+package io.github.simbo1905.tracker;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Nested;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import jdk.sandbox.java.util.json.JsonBoolean;
+import jdk.sandbox.java.util.json.JsonArray;
+import jdk.sandbox.java.util.json.JsonObject;
+import jdk.sandbox.java.util.json.JsonString;
+import jdk.sandbox.java.util.json.JsonValue;
+
+import java.util.Set;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+public class ApiTrackerTest {
+ private static final Logger LOGGER = Logger.getLogger(ApiTrackerTest.class.getName());
+
+ @BeforeAll
+ static void setupLogging() {
+ LoggingControl.setupCleanLogging();
+ }
+
+ @Nested
+ @DisplayName("Local Class Discovery")
+ class LocalDiscoveryTests {
+
+ @Test
+ @DisplayName("Should discover JSON API classes")
+ void testDiscoverLocalJsonClasses() {
+ final var classes = ApiTracker.discoverLocalJsonClasses();
+
+ assertThat(classes).isNotNull();
+ assertThat(classes).isNotEmpty();
+
+ // Should find core JSON interfaces
+ assertThat(classes.stream().map(Class::getName))
+ .contains(
+ "jdk.sandbox.java.util.json.JsonValue",
+ "jdk.sandbox.java.util.json.JsonObject",
+ "jdk.sandbox.java.util.json.JsonArray",
+ "jdk.sandbox.java.util.json.JsonString",
+ "jdk.sandbox.java.util.json.JsonNumber",
+ "jdk.sandbox.java.util.json.JsonBoolean",
+ "jdk.sandbox.java.util.json.JsonNull"
+ );
+
+ // Should NOT find internal implementation classes (public API only)
+ assertThat(classes.stream().anyMatch(c -> c.getName().startsWith("jdk.sandbox.internal.util.json")))
+ .as("Should not find internal implementation classes - public API only")
+ .isFalse();
+
+ // Should be sorted
+ final var names = classes.stream().map(Class::getName).toList();
+ final var sortedNames = names.stream().sorted().toList();
+ assertThat(names).isEqualTo(sortedNames);
+ }
+ }
+
+ @Nested
+ @DisplayName("Local API Extraction")
+ class LocalApiExtractionTests {
+
+ @Test
+ @DisplayName("Should extract API from JsonObject interface")
+ void testExtractLocalApiJsonObject() throws ClassNotFoundException {
+ final var clazz = Class.forName("jdk.sandbox.java.util.json.JsonObject");
+ final var api = ApiTracker.extractLocalApi(clazz);
+
+ assertThat(api).isNotNull();
+ assertThat(api.members()).containsKey("className");
+ assertThat(((JsonString) api.members().get("className")).value()).isEqualTo("JsonObject");
+
+ assertThat(api.members()).containsKey("packageName");
+ assertThat(((JsonString) api.members().get("packageName")).value()).isEqualTo("jdk.sandbox.java.util.json");
+
+ assertThat(api.members()).containsKey("isInterface");
+ assertThat(api.members().get("isInterface")).isEqualTo(JsonBoolean.of(true));
+
+ assertThat(api.members()).containsKey("methods");
+ final var methods = (JsonObject) api.members().get("methods");
+ assertThat(methods.members()).containsKeys("members", "of");
+ }
+
+ @Test
+ @DisplayName("Should extract API from JsonValue sealed interface")
+ void testExtractLocalApiJsonValue() throws ClassNotFoundException {
+ final var clazz = Class.forName("jdk.sandbox.java.util.json.JsonValue");
+ final var api = ApiTracker.extractLocalApi(clazz);
+
+ assertThat(api.members()).containsKey("isSealed");
+ assertThat(api.members().get("isSealed")).isEqualTo(JsonBoolean.of(true));
+
+ assertThat(api.members()).containsKey("permits");
+ final var permits = (JsonArray) api.members().get("permits");
+ assertThat(permits.values()).isNotEmpty();
+ }
+
+ @Test
+ @DisplayName("Should handle null class parameter")
+ void testExtractLocalApiNull() {
+ assertThatThrownBy(() -> ApiTracker.extractLocalApi(null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("clazz must not be null");
+ }
+ }
+
+ @Nested
+ @DisplayName("Upstream Source Fetching")
+ class UpstreamFetchingTests {
+
+ @Test
+ @DisplayName("Should map local class names to upstream paths")
+ void testMapToUpstreamPath() {
+ assertThat(ApiTracker.mapToUpstreamPath("jdk.sandbox.java.util.json.JsonObject"))
+ .isEqualTo("java/util/json/JsonObject.java");
+
+ assertThat(ApiTracker.mapToUpstreamPath("jdk.sandbox.internal.util.json.JsonObjectImpl"))
+ .isEqualTo("jdk/internal/util/json/JsonObjectImpl.java");
+ }
+
+ @Test
+ @DisplayName("Should handle null parameter in fetchUpstreamSources")
+ void testFetchUpstreamSourcesNull() {
+ assertThatThrownBy(() -> ApiTracker.fetchUpstreamSources(null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("localClasses must not be null");
+ }
+
+ @Test
+ @DisplayName("Should return empty map for empty input")
+ void testFetchUpstreamSourcesEmpty() {
+ final var result = ApiTracker.fetchUpstreamSources(Set.of());
+ assertThat(result).isEmpty();
+ }
+ }
+
+ @Nested
+ @DisplayName("API Comparison")
+ class ApiComparisonTests {
+
+ @Test
+ @DisplayName("Should handle null parameters in compareApis")
+ void testCompareApisNull() {
+ final var dummyApi = JsonObject.of(Map.of("className", JsonString.of("Test")));
+
+ assertThatThrownBy(() -> ApiTracker.compareApis(null, dummyApi))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("local must not be null");
+
+ assertThatThrownBy(() -> ApiTracker.compareApis(dummyApi, null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("upstream must not be null");
+ }
+
+ @Test
+ @DisplayName("Should handle upstream errors in comparison")
+ void testCompareApisUpstreamError() {
+ final var local = JsonObject.of(Map.of("className", JsonString.of("TestClass")));
+ final var upstream = JsonObject.of(Map.of(
+ "error", JsonString.of("NOT_FOUND: File not found"),
+ "className", JsonString.of("TestClass")
+ ));
+
+ final var result = ApiTracker.compareApis(local, upstream);
+
+ assertThat(result.members()).containsKey("status");
+ assertThat(((JsonString) result.members().get("status")).value()).isEqualTo("UPSTREAM_ERROR");
+ assertThat(result.members()).containsKey("error");
+ }
+ }
+
+ @Nested
+ @DisplayName("Full Comparison Orchestration")
+ class FullComparisonTests {
+
+ @Test
+ @DisplayName("Should run full comparison and return report structure")
+ void testRunFullComparison() {
+ final var report = ApiTracker.runFullComparison();
+
+ assertThat(report).isNotNull();
+ assertThat(report.members()).containsKeys(
+ "timestamp",
+ "localPackage",
+ "upstreamPackage",
+ "summary",
+ "differences",
+ "durationMs"
+ );
+
+ final var summary = (JsonObject) report.members().get("summary");
+ assertThat(summary.members()).containsKeys(
+ "totalClasses",
+ "matchingClasses",
+ "missingUpstream",
+ "differentApi"
+ );
+
+ // Total classes should be greater than 0
+ final var totalClasses = summary.members().get("totalClasses");
+ assertThat(totalClasses).isNotNull();
+ }
+ }
+
+ @Nested
+ @DisplayName("Modifier Extraction")
+ class ModifierExtractionTests {
+
+ @Test
+ @DisplayName("Should extract modifiers correctly")
+ void testExtractModifiers() {
+ // Test public static final
+ final var modifiers = java.lang.reflect.Modifier.PUBLIC |
+ java.lang.reflect.Modifier.STATIC |
+ java.lang.reflect.Modifier.FINAL;
+
+ final var result = ApiTracker.extractModifiers(modifiers);
+
+ assertThat(result.values()).hasSize(3);
+ assertThat(result.values().stream().map(v -> ((JsonString) v).value()))
+ .containsExactlyInAnyOrder("public", "static", "final");
+ }
+ }
+}
\ No newline at end of file
diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java
new file mode 100644
index 0000000..d54522a
--- /dev/null
+++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java
@@ -0,0 +1,52 @@
+package io.github.simbo1905.tracker;
+
+import java.util.logging.*;
+
+/// Modern Java companion object pattern for test logging configuration.
+/// Uses sealed interface with config record to avoid singleton anti-patterns.
+/// Provides clean, compact output instead of JUL's ugly two-line default format.
+public sealed interface LoggingControl permits LoggingControl.Config {
+
+ /// Configuration record for logging setup
+ record Config(Level defaultLevel) implements LoggingControl {
+ }
+
+ /// Set up clean, compact logging format for tests using functional style.
+ /// No instances, no singletons - just clean configuration via records and default methods.
+ static void setupCleanLogging(Config config) {
+ // Allow CLI override via -Djava.util.logging.ConsoleHandler.level=FINER
+ String logLevel = System.getProperty("java.util.logging.ConsoleHandler.level");
+ Level level = (logLevel != null) ? Level.parse(logLevel) : config.defaultLevel();
+
+ // Get the root logger to configure globally
+ Logger rootLogger = Logger.getLogger("");
+
+ // Remove default handlers to prevent ugly JUL formatting
+ for (Handler handler : rootLogger.getHandlers()) {
+ rootLogger.removeHandler(handler);
+ }
+
+ // Create console handler with clean formatting
+ ConsoleHandler consoleHandler = new ConsoleHandler();
+ consoleHandler.setLevel(level);
+
+ // Custom formatter for compact single-line output (saves tokens and money)
+ consoleHandler.setFormatter(new Formatter() {
+ @Override
+ public String format(LogRecord record) {
+ return record.getMessage() + "\n";
+ }
+ });
+
+ rootLogger.addHandler(consoleHandler);
+ rootLogger.setLevel(level);
+ }
+
+ /// Convenience method with default WARNING level
+ static void setupCleanLogging() {
+ Level level = System.getProperty("java.util.logging.ConsoleHandler.level") != null
+ ? Level.parse(System.getProperty("java.util.logging.ConsoleHandler.level"))
+ : Level.WARNING;
+ setupCleanLogging(new Config(level));
+ }
+}
\ No newline at end of file
diff --git a/json-java21-api-tracker/src/test/resources/JsonObject.java b/json-java21-api-tracker/src/test/resources/JsonObject.java
new file mode 100644
index 0000000..ad98b40
--- /dev/null
+++ b/json-java21-api-tracker/src/test/resources/JsonObject.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package java.util.json;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import jdk.internal.javac.PreviewFeature;
+import jdk.internal.util.json.JsonObjectImpl;
+
+/**
+ * The interface that represents JSON object.
+ *
+ *
+ * A {@code JsonObject} can be produced by a {@link Json#parse(String)}.
+ *
+ * Alternatively, {@link #of(Map)} can be used to obtain a {@code JsonObject}.
+ * Implementations of {@code JsonObject} cannot be created from sources that
+ * contain duplicate member names. If duplicate names appear during
+ * a {@link Json#parse(String)}, a {@code JsonParseException} is thrown.
+ *
+ * @since 99
+ */
+@PreviewFeature(feature = PreviewFeature.Feature.JSON)
+public non-sealed interface JsonObject extends JsonValue {
+
+ /**
+ * {@return an unmodifiable map of the {@code String} to {@code JsonValue}
+ * members in this {@code JsonObject}}
+ */
+ Map members();
+
+ /**
+ * {@return the {@code JsonObject} created from the given
+ * map of {@code String} to {@code JsonValue}s}
+ *
+ * The {@code JsonObject}'s members occur in the same order as the given
+ * map's entries.
+ *
+ * @param map the map of {@code JsonValue}s. Non-null.
+ * @throws NullPointerException if {@code map} is {@code null}, contains
+ * any keys that are {@code null}, or contains any values that are {@code null}.
+ */
+ static JsonObject of(Map map) {
+ return new JsonObjectImpl(map.entrySet() // Implicit NPE on map
+ .stream()
+ .collect(Collectors.toMap(
+ e -> Objects.requireNonNull(e.getKey()),
+ Map.Entry::getValue, // Implicit NPE on val
+ (_, v) -> v,
+ LinkedHashMap::new)));
+ }
+
+ /**
+ * {@return {@code true} if the given object is also a {@code JsonObject}
+ * and the two {@code JsonObject}s represent the same mappings} Two
+ * {@code JsonObject}s {@code jo1} and {@code jo2} represent the same
+ * mappings if {@code jo1.members().equals(jo2.members())}.
+ *
+ * @see #members()
+ */
+ @Override
+ boolean equals(Object obj);
+
+ /**
+ * {@return the hash code value for this {@code JsonObject}} The hash code value
+ * of a {@code JsonObject} is derived from the hash code of {@code JsonObject}'s
+ * {@link #members()}. Thus, for two {@code JsonObject}s {@code jo1} and {@code jo2},
+ * {@code jo1.equals(jo2)} implies that {@code jo1.hashCode() == jo2.hashCode()}
+ * as required by the general contract of {@link Object#hashCode}.
+ *
+ * @see #members()
+ */
+ @Override
+ int hashCode();
+}
diff --git a/json-java21/pom.xml b/json-java21/pom.xml
new file mode 100644
index 0000000..10ade61
--- /dev/null
+++ b/json-java21/pom.xml
@@ -0,0 +1,35 @@
+
+
+ 4.0.0
+
+
+ io.github.simbo1905.json
+ json-java21-parent
+ 0.1-SNAPSHOT
+
+
+ json-java21
+ jar
+
+ java.util.json Backport
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
diff --git a/src/main/java/jdk/sandbox/demo/JsonDemo.java b/json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java
similarity index 100%
rename from src/main/java/jdk/sandbox/demo/JsonDemo.java
rename to json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java
diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java
similarity index 100%
rename from src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java
rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java
diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java
similarity index 100%
rename from src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java
rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java
diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java
similarity index 100%
rename from src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java
rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java
diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java
similarity index 100%
rename from src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java
rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java
diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java
similarity index 100%
rename from src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java
rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java
diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java
similarity index 100%
rename from src/main/java/jdk/sandbox/internal/util/json/JsonParser.java
rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java
diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java
similarity index 100%
rename from src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java
rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java
diff --git a/src/main/java/jdk/sandbox/internal/util/json/StableValue.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java
similarity index 100%
rename from src/main/java/jdk/sandbox/internal/util/json/StableValue.java
rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java
diff --git a/src/main/java/jdk/sandbox/internal/util/json/Utils.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java
similarity index 100%
rename from src/main/java/jdk/sandbox/internal/util/json/Utils.java
rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/Json.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/Json.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonArray.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/JsonArray.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonNull.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/JsonNull.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/JsonNumber.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonObject.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/JsonObject.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/JsonParseException.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonString.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/JsonString.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonValue.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/JsonValue.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java
diff --git a/src/main/java/jdk/sandbox/java/util/json/package-info.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java
similarity index 100%
rename from src/main/java/jdk/sandbox/java/util/json/package-info.java
rename to json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java
diff --git a/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java
similarity index 100%
rename from src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java
rename to json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java
diff --git a/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java
similarity index 100%
rename from src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java
rename to json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java
diff --git a/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java
similarity index 100%
rename from src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java
rename to json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java
diff --git a/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java
similarity index 100%
rename from src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java
rename to json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java
diff --git a/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java
similarity index 100%
rename from src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java
rename to json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java
diff --git a/mvn-test-no-boilerplate.sh b/mvn-test-no-boilerplate.sh
new file mode 100755
index 0000000..4142448
--- /dev/null
+++ b/mvn-test-no-boilerplate.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+
+# Strip Maven test boilerplate - show compile errors and test results only
+# Usage: ./mvn-test-no-boilerplate.sh [maven test arguments]
+#
+# Examples:
+# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests
+# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=INFO
+# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=FINER
+#
+# For running tests in a specific module:
+# ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Dtest=CompilerApiLearningTest
+#
+# The script automatically detects if mvnd is available, otherwise falls back to mvn
+
+# Detect if mvnd is available, otherwise use mvn
+if command -v mvnd &> /dev/null; then
+ MVN_CMD="mvnd"
+else
+ MVN_CMD="mvn"
+fi
+
+$MVN_CMD test "$@" 2>&1 | awk '
+BEGIN {
+ scanning_started = 0
+ compilation_section = 0
+ test_section = 0
+}
+
+# Skip all WARNING lines before project scanning starts
+/INFO.*Scanning for projects/ {
+ scanning_started = 1
+ print
+ next
+}
+
+# Before scanning starts, skip WARNING lines
+!scanning_started && /^WARNING:/ { next }
+
+# Show compilation errors
+/COMPILATION ERROR/ { compilation_section = 1 }
+/BUILD FAILURE/ && compilation_section { compilation_section = 0 }
+
+# Show test section
+/INFO.*T E S T S/ {
+ test_section = 1
+ print "-------------------------------------------------------"
+ print " T E S T S"
+ print "-------------------------------------------------------"
+ next
+}
+
+# In compilation error section, show everything
+compilation_section { print }
+
+# In test section, show everything - let user control logging with -D arguments
+test_section {
+ print
+}
+
+# Before test section starts, show important lines only
+!test_section && scanning_started {
+ if (/INFO.*Scanning|INFO.*Building|INFO.*resources|INFO.*compiler|INFO.*surefire|ERROR|FAILURE/) {
+ print
+ }
+ # Show compilation warnings/errors
+ if (/WARNING.*COMPILATION|ERROR.*/) {
+ print
+ }
+}
+'
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 03aca4c..086aeb5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,80 +1,123 @@
-
- 4.0.0
+
+
+ 4.0.0
- jdk-sandbox
- json-experimental
- 0.1-SNAPSHOT
- jar
+ io.github.simbo1905.json
+ json-java21-parent
+ 0.1-SNAPSHOT
+ pom
- java.util.json Backport for JDK 21+
- Early access to future java.util.json API - tracking OpenJDK sandbox development
- https://simbo1905.github.io/java.util.json.Java21/
+ java.util.json Backport Parent
+ A backport of the upcoming java.util.json API for Java 21+
+ https://simbo1905.github.io/java.util.json.Java21/
-
-
- GNU General Public License, version 2, with the Classpath Exception
- https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
- repo
-
-
+
+
+ GNU General Public License, version 2, with the Classpath Exception
+ https://openjdk.org/legal/gplv2+ce.html
+
+
-
- 21
- 21
- 5.13.1
- 1.13.1
-
+
+
+ Simon
+ simon@simon.com
+ simon
+ https://github.com/simbo1905
+
+
-
-
- org.junit.jupiter
- junit-jupiter-api
- ${junit.jupiter.version}
- test
-
-
- org.junit.jupiter
- junit-jupiter-engine
- ${junit.jupiter.version}
- test
-
-
- org.junit.platform
- junit-platform-launcher
- ${junit.platform.version}
- test
-
-
- org.junit.platform
- junit-platform-console
- ${junit.platform.version}
- test
-
-
- org.assertj
- assertj-core
- 3.26.3
- test
-
-
+
+ scm:git:git://github.com/simbo1905/java.util.json.Java21.git
+ scm:git:ssh://github.com:simbo1905/java.util.json.Java21.git
+ https://github.com/simbo1905/java.util.json.Java21/tree/main
+
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.8.1
-
- 21
- 21
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- 3.2.5
-
-
-
-
+
+ json-java21
+ json-java21-api-tracker
+
+
+
+ UTF-8
+ 21
+ 5.10.2
+ 3.25.3
+
+
+ 3.4.0
+ 3.3.1
+ 3.13.0
+ 3.2.5
+ 3.4.2
+ 3.1.2
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.jupiter.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-clean-plugin
+ ${maven-clean-plugin.version}
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ ${maven-resources-plugin.version}
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven-compiler-plugin.version}
+
+
+ -Xlint:all
+ -Werror
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ ${maven-jar-plugin.version}
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+ ${maven-install-plugin.version}
+
+
+
+
+
\ No newline at end of file