From 05577c5a3b11a546eaf70fe09a9856206f23e815 Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:35:55 +0000 Subject: [PATCH 1/4] backport examples --- json-java21/AGENTS.md | 208 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 json-java21/AGENTS.md diff --git a/json-java21/AGENTS.md b/json-java21/AGENTS.md new file mode 100644 index 0000000..b82def4 --- /dev/null +++ b/json-java21/AGENTS.md @@ -0,0 +1,208 @@ +# json-java21 Module AGENTS.md + +## Purpose +This module backports the upstream OpenJDK sandbox `java.util.json` API to Java 21. + +## Upstream Source +- Repository: https://github.com/openjdk/jdk-sandbox +- Branch: `json` (NOT master!) +- Base path: `src/java.base/share/classes/` +- Public API: `java/util/json/*.java` +- Internal implementation: `jdk/internal/util/json/*.java` + +## CRITICAL WARNING + +**DO NOT DOWNLOAD THE REPOSITORY ZIP FILE!** + +The jdk-sandbox repository is MASSIVE (the entire JDK). We only need ~19 small Java files. + +**ALWAYS fetch individual files using raw GitHub URLs one at a time.** + +## Sync Process + +### Step 1: Prepare Fresh Download Area +```bash +rm -rf .tmp/upstream-sync +mkdir -p .tmp/upstream-sync/java/util/json +mkdir -p .tmp/upstream-sync/jdk/internal/util/json +``` + +### Step 2: Fetch Upstream Sources (ONE FILE AT A TIME) + +**CRITICAL: Fetch each file individually using curl or wget with the raw GitHub URL.** + +The URL pattern is: +``` +https://raw.githubusercontent.com/openjdk/jdk-sandbox/json/src/java.base/share/classes/ +``` + +Note the branch is `json` in the URL path (NOT `refs/heads/json`, just `json`). + +#### Public API files (~10 files): +```bash +curl -o .tmp/upstream-sync/java/util/json/Json.java \ + "https://raw.githubusercontent.com/openjdk/jdk-sandbox/json/src/java.base/share/classes/java/util/json/Json.java" + +curl -o .tmp/upstream-sync/java/util/json/JsonArray.java \ + "https://raw.githubusercontent.com/openjdk/jdk-sandbox/json/src/java.base/share/classes/java/util/json/JsonArray.java" + +... +``` + +#### Internal implementation files (~9 files): +```bash +curl -o .tmp/upstream-sync/jdk/internal/util/json/JsonArrayImpl.java \ + "https://raw.githubusercontent.com/openjdk/jdk-sandbox/json/src/java.base/share/classes/jdk/internal/util/json/JsonArrayImpl.java" + +curl -o .tmp/upstream-sync/jdk/internal/util/json/JsonBooleanImpl.java \ + "https://raw.githubusercontent.com/openjdk/jdk-sandbox/json/src/java.base/share/classes/jdk/internal/util/json/JsonBooleanImpl.java" + +... +``` + +#### Verify downloads succeeded: +```bash +# Should show X files (whatever is currently upstream) +ls -la .tmp/upstream-sync/java/util/json/ + +# Should show Y files (whatever is currently upstream) +ls -la .tmp/upstream-sync/jdk/internal/util/json/ + +# Check none are empty or HTML error pages +wc -l .tmp/upstream-sync/java/util/json/*.java +wc -l .tmp/upstream-sync/jdk/internal/util/json/*.java +``` + +### Step 3: Create Backported Structure +Create parallel structure in `.tmp/backported/` with our package names: + +```bash +mkdir -p .tmp/backported/jdk/sandbox/java/util/json +mkdir -p .tmp/backported/jdk/sandbox/internal/util/json +``` + +### Step 4: Apply Backporting Transformations +For each downloaded file, apply these transformations using Python heredocs (not sed/perl for multi-line): + +#### 4.1 Package Renaming +- `java.util.json` → `jdk.sandbox.java.util.json` +- `jdk.internal.util.json` → `jdk.sandbox.internal.util.json` + +#### 4.2 Remove Preview Feature Annotations +Delete lines containing: +- `import jdk.internal.javac.PreviewFeature;` +- `@PreviewFeature(feature = PreviewFeature.Feature.JSON)` + +#### 4.3 StableValue Polyfill +Upstream uses `jdk.internal.lang.stable.StableValue` which is not available in Java 21. + +**Replace imports:** +- `import jdk.internal.lang.stable.StableValue;` → (remove, our polyfill is package-local) + +**The polyfill `StableValue.java`** (already in our repo) provides: +- `StableValue.of()` - creates empty holder +- `orElse(T defaultValue)` - returns value or default +- `orElseSet(Supplier)` - lazy initialization with double-checked locking +- `setOrThrow(T)` - one-time set +- `StableValue.supplier(Supplier)` - memoizing supplier wrapper + +This file is NOT from upstream and must be preserved during sync. + +#### 4.4 DO NOT Convert JavaDoc to JEP 467 Markdown +If upstream uses `/** ... */` style, DO NOT convert them to our `/// ...` format; we will not edit the upstream files more than the absolute minimum to get them to run on Java 21. + +#### 4.5 Add JsonAssertionException (Our Addition) +The file `JsonAssertionException.java` is a local addition not in upstream. Preserve it. + +#### 4.6 Preserve Demo File +The file `jdk/sandbox/demo/JsonDemo.java` is a local addition for demonstration purposes. Preserve it. Fix it. + +### Step 5: Verify Compilation with javac +Before copying to the main source tree, verify the backported code compiles: + +```bash +# Find all Java files in the backported structure +find .tmp/backported -name "*.java" > .tmp/sources.txt + +# Also include our polyfill and local additions +echo "json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java" >> .tmp/sources.txt +echo "json-java21/src/main/java/jdk/sandbox/java/util/json/JsonAssertionException.java" >> .tmp/sources.txt + +# Compile with Java 21 +javac --release 21 -d .tmp/classes @.tmp/sources.txt +``` + +### Step 6: Copy to Source Tree (After Verification) + +Only after javac succeeds: + +```bash +# Backup current sources (optional) +cp -r json-java21/src/main/java/jdk/sandbox .tmp/backup-sandbox + +# Copy backported files (excluding our local additions) +cp .tmp/backported/jdk/sandbox/java/util/json/*.java \ + json-java21/src/main/java/jdk/sandbox/java/util/json/ + +cp .tmp/backported/jdk/sandbox/internal/util/json/*.java \ + json-java21/src/main/java/jdk/sandbox/internal/util/json/ + +# Restore our local additions if overwritten +# (StableValue.java, JsonAssertionException.java should not be in backported/) +``` + +The file `jdk/sandbox/demo/JsonDemo.java` should be the example code in our README.md, as it may have changed to reflect upstream changes. You MUST update the README.md to include examples of the upgraded code in this file, which you must MANUALLY VERIFY IS GOOD post-upgrade. + +### Step 7: Full Maven Build + +```bash +$(command -v mvnd || command -v mvn || command -v ./mvnw) clean test -pl json-java21 -Djava.util.logging.ConsoleHandler.level=INFO +``` + +## Files That Are Local Additions (Preserve During Sync) + +| File | Purpose | +|------|---------| +| `jdk/sandbox/internal/util/json/StableValue.java` | Java 21 polyfill for future JDK StableValue API | +| `jdk/sandbox/java/util/json/JsonAssertionException.java` | Custom exception for type assertion errors | +| `jdk/sandbox/demo/JsonDemo.java` | Demonstration/example code | + +## Transformation Example + +**Upstream `JsonStringImpl.java` (excerpt):** +```java +package jdk.internal.util.json; + +import java.util.json.JsonString; +import jdk.internal.lang.stable.StableValue; + +public final class JsonStringImpl implements JsonString, JsonValueImpl { + private final StableValue jsonStr = StableValue.of(); + // ... +} +``` + +**Backported version:** +```java +package jdk.sandbox.internal.util.json; + +import jdk.sandbox.java.util.json.JsonString; +// StableValue is package-local, no import needed + +public final class JsonStringImpl implements JsonString, JsonValueImpl { + private final StableValue jsonStr = StableValue.of(); + // ... +} +``` + +## Troubleshooting + +### Compilation Errors After Sync +1. Check package names are correctly transformed +2. Verify StableValue polyfill is present +3. Check for new upstream APIs that may need additional polyfills + +### Test Failures After Sync +1. Run with verbose logging: `-Djava.util.logging.ConsoleHandler.level=FINE` +2. Check if upstream changed method signatures +3. Review upstream commit history for breaking changes From 26c47212e15faeaa9ee36064bd69170143af5bf7 Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:00:19 +0000 Subject: [PATCH 2/4] Sync upstream commit 91a479d: Reduce StringBuilder allocation in toDisplayString Upstream API changes: - JsonString.value() -> JsonString.string() - JsonNumber.toNumber() -> JsonNumber.toLong()/toDouble() - JsonBoolean.value() -> JsonBoolean.bool() - JsonArray.values() -> JsonArray.elements() - Json.fromUntyped()/toUntyped() removed - New navigation: JsonValue.get(String), element(int), getOrAbsent() Backporting: - Added LazyConstant.java polyfill (upstream switched from StableValue) - Added Utils.powExact() polyfill for Math.powExact(long, int) - Replaced unnamed variables _ with ignored (Java 21 compat) - Updated all modules to use new API Verify: mvnd verify (390 tests pass) --- .gitignore | 10 + README.md | 129 ++++--- .../github/simbo1905/tracker/ApiTracker.java | 74 ++-- .../simbo1905/tracker/ApiTrackerTest.java | 12 +- .../src/main/java/json/java21/jtd/Jtd.java | 22 +- .../main/java/json/java21/jtd/JtdSchema.java | 41 +- .../java/json/java21/jtd/JtdPropertyTest.java | 10 +- .../java/json/java21/jtd/TestRfc8927.java | 37 +- .../internal/util/json/JsonArrayImpl.java | 14 +- .../internal/util/json/JsonBooleanImpl.java | 9 +- .../internal/util/json/JsonNullImpl.java | 1 + .../internal/util/json/JsonNumberImpl.java | 139 ++++--- .../internal/util/json/JsonObjectImpl.java | 4 +- .../internal/util/json/JsonParser.java | 69 ++-- .../internal/util/json/JsonStringImpl.java | 36 +- .../internal/util/json/JsonValueImpl.java | 18 +- .../internal/util/json/LazyConstant.java | 36 ++ .../jdk/sandbox/internal/util/json/Utils.java | 83 ++-- .../java/jdk/sandbox/java/util/json/Json.java | 355 +++++------------- .../jdk/sandbox/java/util/json/JsonArray.java | 93 +++-- .../sandbox/java/util/json/JsonBoolean.java | 69 ++-- .../jdk/sandbox/java/util/json/JsonNull.java | 26 +- .../sandbox/java/util/json/JsonNumber.java | 278 +++++++------- .../sandbox/java/util/json/JsonObject.java | 95 +++-- .../java/util/json/JsonParseException.java | 64 ++-- .../sandbox/java/util/json/JsonString.java | 121 +++--- .../jdk/sandbox/java/util/json/JsonValue.java | 346 +++++++++++++---- .../sandbox/java/util/json/package-info.java | 207 +++------- .../internal/util/json/JsonParserTests.java | 24 +- .../util/json/JsonPatternMatchingTests.java | 8 +- .../util/json/JsonRecordMappingTests.java | 18 +- .../java/util/json/EscapedKeyBugTest.java | 10 +- .../java/util/json/JsonTypedUntypedTests.java | 236 ------------ .../java/util/json/ReadmeDemoTests.java | 91 ++--- 34 files changed, 1311 insertions(+), 1474 deletions(-) create mode 100644 json-java21/src/main/java/jdk/sandbox/internal/util/json/LazyConstant.java delete mode 100644 json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java diff --git a/.gitignore b/.gitignore index 3c1106f..1143434 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,19 @@ +# Eclipse/IDE files +.project +.classpath +.settings/ json-compatibility-suite/.classpath json-compatibility-suite/.project json-compatibility-suite/.settings/ json-java21/.classpath json-java21/.project json-java21/.settings/ +json-java21-api-tracker/.classpath +json-java21-api-tracker/.project +json-java21-api-tracker/.settings/ +json-java21-jtd/.classpath +json-java21-jtd/.project +json-java21-jtd/.settings/ json-java21-schema/src/test/resources/draft4/ json-java21-schema/src/test/resources/json-schema-test-suite-data/ diff --git a/README.md b/README.md index 4f86841..dc355a1 100644 --- a/README.md +++ b/README.md @@ -41,16 +41,16 @@ JsonValue value = Json.parse(json); // Access as map-like structure JsonObject obj = (JsonObject) value; -String name = ((JsonString) obj.members().get("name")).value(); -int age = ((JsonNumber) obj.members().get("age")).intValue(); -boolean active = ((JsonBoolean) obj.members().get("active")).value(); +String name = ((JsonString) obj.members().get("name")).string(); +long age = ((JsonNumber) obj.members().get("age")).toLong(); +boolean active = ((JsonBoolean) obj.members().get("active")).bool(); ``` ### Simple Record Mapping ```java // Define records for structured data -record User(String name, int age, boolean active) {} +record User(String name, long age, boolean active) {} // Parse JSON directly to records String userJson = "{\"name\":\"Bob\",\"age\":25,\"active\":false}"; @@ -58,55 +58,60 @@ JsonObject jsonObj = (JsonObject) Json.parse(userJson); // Map to record User user = new User( - ((JsonString) jsonObj.members().get("name")).value(), - ((JsonNumber) jsonObj.members().get("age")).intValue(), - ((JsonBoolean) jsonObj.members().get("active")).value() + ((JsonString) jsonObj.members().get("name")).string(), + ((JsonNumber) jsonObj.members().get("age")).toLong(), + ((JsonBoolean) jsonObj.members().get("active")).bool() ); -// Convert records back to JSON -JsonValue backToJson = Json.fromUntyped(Map.of( - "name", user.name(), - "age", user.age(), - "active", user.active() +// Convert records back to JSON using typed factories +JsonValue backToJson = JsonObject.of(Map.of( + "name", JsonString.of(user.name()), + "age", JsonNumber.of(user.age()), + "active", JsonBoolean.of(user.active()) )); -// Covert back to a JSON string +// Convert back to a JSON string String jsonString = backToJson.toString(); ``` -### Converting from Java Objects to JSON (`fromUntyped`) +### Building JSON Programmatically ```java -// Convert standard Java collections to JsonValue -Map data = Map.of( - "name", "John", - "age", 30, - "scores", List.of(85, 92, 78) -); -JsonValue json = Json.fromUntyped(data); +// Build JSON using typed factory methods +JsonObject data = JsonObject.of(Map.of( + "name", JsonString.of("John"), + "age", JsonNumber.of(30), + "scores", JsonArray.of(List.of( + JsonNumber.of(85), + JsonNumber.of(92), + JsonNumber.of(78) + )) +)); +String json = data.toString(); ``` -### Converting from JSON to Java Objects (`toUntyped`) +### Extracting Values from JSON ```java -// Convert JsonValue back to standard Java types +// Extract values from parsed JSON JsonValue parsed = Json.parse("{\"name\":\"John\",\"age\":30}"); -Object data = Json.toUntyped(parsed); -// Returns a Map with standard Java types -``` +JsonObject obj = (JsonObject) parsed; -The conversion mappings are: -- `JsonObject` ↔ `Map` -- `JsonArray` ↔ `List` -- `JsonString` ↔ `String` -- `JsonNumber` ↔ `Number` (Long, Double, BigInteger, or BigDecimal) -- `JsonBoolean` ↔ `Boolean` -- `JsonNull` ↔ `null` +// Use the new type-safe accessor methods +String name = obj.get("name").string(); // Returns "John" +long age = obj.get("age").toLong(); // Returns 30L +double ageDouble = obj.get("age").toDouble(); // Returns 30.0 +``` -This is useful for: -- Integrating with existing code that uses standard collections -- Serializing/deserializing to formats that expect Java types -- Working with frameworks that use reflection on standard types +The accessor methods on `JsonValue`: +- `string()` - Returns the String value (for JsonString) +- `toLong()` - Returns the long value (for JsonNumber, if representable) +- `toDouble()` - Returns the double value (for JsonNumber, if representable) +- `bool()` - Returns the boolean value (for JsonBoolean) +- `elements()` - Returns List (for JsonArray) +- `members()` - Returns Map (for JsonObject) +- `get(String name)` - Access JsonObject member by name +- `element(int index)` - Access JsonArray element by index ### Realistic Record Mapping @@ -123,29 +128,29 @@ Team team = new Team("Engineering", List.of( new User("Bob", "bob@example.com", false) )); -// Convert records to JSON -JsonValue teamJson = Json.fromUntyped(Map.of( - "teamName", team.teamName(), - "members", team.members().stream() - .map(u -> Map.of( - "name", u.name(), - "email", u.email(), - "active", u.active() - )) - .toList() +// Convert records to JSON using typed factories +JsonValue teamJson = JsonObject.of(Map.of( + "teamName", JsonString.of(team.teamName()), + "members", JsonArray.of(team.members().stream() + .map(u -> JsonObject.of(Map.of( + "name", JsonString.of(u.name()), + "email", JsonString.of(u.email()), + "active", JsonBoolean.of(u.active()) + ))) + .toList()) )); // Parse JSON back to records JsonObject parsed = (JsonObject) Json.parse(teamJson.toString()); Team reconstructed = new Team( - ((JsonString) parsed.members().get("teamName")).value(), - ((JsonArray) parsed.members().get("members")).values().stream() + ((JsonString) parsed.members().get("teamName")).string(), + ((JsonArray) parsed.members().get("members")).elements().stream() .map(v -> { JsonObject member = (JsonObject) v; return new User( - ((JsonString) member.members().get("name")).value(), - ((JsonString) member.members().get("email")).value(), - ((JsonBoolean) member.members().get("active")).value() + ((JsonString) member.members().get("name")).string(), + ((JsonString) member.members().get("email")).string(), + ((JsonBoolean) member.members().get("active")).bool() ); }) .toList() @@ -182,10 +187,10 @@ Process JSON arrays efficiently with Java streams: ```java // Filter active users from a JSON array JsonArray users = (JsonArray) Json.parse(jsonArrayString); -List activeUserEmails = users.values().stream() +List activeUserEmails = users.elements().stream() .map(v -> (JsonObject) v) - .filter(obj -> ((JsonBoolean) obj.members().get("active")).value()) - .map(obj -> ((JsonString) obj.members().get("email")).value()) + .filter(obj -> ((JsonBoolean) obj.members().get("active")).bool()) + .map(obj -> ((JsonString) obj.members().get("email")).string()) .toList(); ``` @@ -263,7 +268,14 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json" ## Current Status -This code (as of 2025-09-04) is derived from the OpenJDK jdk-sandbox repository “json” branch at commit [a8e7de8b49e4e4178eb53c94ead2fa2846c30635](https://github.com/openjdk/jdk-sandbox/commit/a8e7de8b49e4e4178eb53c94ead2fa2846c30635) ("Produce path/col during path building", 2025-08-14 UTC). +This code (as of 2026-01-25) is derived from the OpenJDK jdk-sandbox repository "json" branch. Key API changes from the previous version include: +- `JsonString.value()` → `JsonString.string()` +- `JsonNumber.toNumber()` → `JsonNumber.toLong()` / `JsonNumber.toDouble()` +- `JsonBoolean.value()` → `JsonBoolean.bool()` +- `JsonArray.values()` → `JsonArray.elements()` +- `Json.fromUntyped()` and `Json.toUntyped()` have been removed +- New accessor methods on `JsonValue`: `get(String)`, `element(int)`, `getOrAbsent(String)`, `valueOrNull()` +- Internal implementation changed from `StableValue` to `LazyConstant` The original proposal and design rationale can be found in the included PDF: [Towards a JSON API for the JDK.pdf](Towards%20a%20JSON%20API%20for%20the%20JDK.pdf) @@ -276,8 +288,11 @@ A daily workflow runs an API comparison against the OpenJDK sandbox and prints a ## Modifications This is a simplified backport with the following changes from the original: -- Replaced `StableValue.of()` with double-checked locking pattern. +- Replaced `LazyConstant` with a package-local polyfill using double-checked locking pattern. +- Added `Utils.powExact()` polyfill for `Math.powExact(long, int)` which is not available in Java 21. +- Replaced unnamed variables `_` with `ignored` for Java 21 compatibility. - Removed `@ValueBased` annotations. +- Removed `@PreviewFeature` annotations. - Compatible with JDK 21. ## Security Considerations diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java index 589363f..1ee8075 100644 --- a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java @@ -599,7 +599,7 @@ static JsonObject compareApis(JsonObject local, JsonObject upstream) { // Extract class name safely final var localClassName = local.members().get("className"); final var className = localClassName instanceof JsonString js ? - js.value() : "Unknown"; + js.string() : "Unknown"; diffMap.put("className", JsonString.of(className)); @@ -612,7 +612,7 @@ static JsonObject compareApis(JsonObject local, JsonObject upstream) { // Check if status is NOT_IMPLEMENTED (from parsing) if (upstream.members().containsKey("status")) { - final var status = ((JsonString) upstream.members().get("status")).value(); + final var status = ((JsonString) upstream.members().get("status")).string(); if ("NOT_IMPLEMENTED".equals(status)) { diffMap.put("status", JsonString.of("PARSE_NOT_IMPLEMENTED")); return JsonObject.of(diffMap); @@ -681,11 +681,11 @@ static boolean compareModifiers(JsonObject local, JsonObject upstream, List ((JsonString) v).value()) + final var localSet = localMods.elements().stream() + .map(v -> ((JsonString) v).string()) .collect(Collectors.toSet()); - final var upstreamSet = upstreamMods.values().stream() - .map(v -> ((JsonString) v).value()) + final var upstreamSet = upstreamMods.elements().stream() + .map(v -> ((JsonString) v).string()) .collect(Collectors.toSet()); if (!localSet.equals(upstreamSet)) { @@ -708,11 +708,11 @@ static boolean compareInheritance(JsonObject local, JsonObject upstream, List normalizeTypeName(((JsonString) v).value())) + final var localTypes = localExtends.elements().stream() + .map(v -> normalizeTypeName(((JsonString) v).string())) .collect(Collectors.toSet()); - final var upstreamTypes = upstreamExtends.values().stream() - .map(v -> normalizeTypeName(((JsonString) v).value())) + final var upstreamTypes = upstreamExtends.elements().stream() + .map(v -> normalizeTypeName(((JsonString) v).string())) .collect(Collectors.toSet()); if (!localTypes.equals(upstreamTypes)) { @@ -786,8 +786,8 @@ static boolean compareMethods(JsonObject local, JsonObject upstream, List matchingCount++; case "UPSTREAM_ERROR" -> missingUpstream++; @@ -964,21 +964,21 @@ static String generateFingerprint(JsonObject report) { final var differences = (JsonArray) report.members().get("differences"); final var stableLines = new ArrayList(); - for (final var diff : differences.values()) { + for (final var diff : differences.elements()) { final var diffObj = (JsonObject) diff; - final var status = ((JsonString) diffObj.members().get("status")).value(); + final var status = ((JsonString) diffObj.members().get("status")).string(); if (!"DIFFERENT".equals(status)) continue; - final var className = ((JsonString) diffObj.members().get("className")).value(); + final var className = ((JsonString) diffObj.members().get("className")).string(); final var classDiffs = (JsonArray) diffObj.members().get("differences"); if (classDiffs != null) { - for (final var change : classDiffs.values()) { + for (final var change : classDiffs.elements()) { final var changeObj = (JsonObject) change; - final var type = ((JsonString) changeObj.members().get("type")).value(); + final var type = ((JsonString) changeObj.members().get("type")).string(); final var methodValue = changeObj.members().get("method"); - final var method = methodValue instanceof JsonString js ? js.value() : ""; + final var method = methodValue instanceof JsonString js ? js.string() : ""; // Create stable line: "ClassName:changeType:methodName" stableLines.add(className + ":" + type + ":" + method); } @@ -1013,7 +1013,7 @@ private static long getDifferentApiCount(JsonObject report) { } final var differentApiValue = summary.members().get("differentApi"); if (differentApiValue instanceof JsonNumber num) { - return num.toNumber().longValue(); + return num.toLong(); } return 0; } @@ -1027,10 +1027,10 @@ static String generateSummary(JsonObject report) { final var summary = (JsonObject) report.members().get("summary"); final var differences = (JsonArray) report.members().get("differences"); - final var totalClasses = ((JsonNumber) summary.members().get("totalClasses")).toNumber().longValue(); - final var matchingClasses = ((JsonNumber) summary.members().get("matchingClasses")).toNumber().longValue(); + final var totalClasses = ((JsonNumber) summary.members().get("totalClasses")).toLong(); + final var matchingClasses = ((JsonNumber) summary.members().get("matchingClasses")).toLong(); final var differentApi = getDifferentApiCount(report); - final var missingUpstream = ((JsonNumber) summary.members().get("missingUpstream")).toNumber().longValue(); + final var missingUpstream = ((JsonNumber) summary.members().get("missingUpstream")).toLong(); sb.append("## API Comparison Summary\n\n"); sb.append("| Metric | Count |\n"); @@ -1043,22 +1043,22 @@ static String generateSummary(JsonObject report) { if (differentApi > 0) { sb.append("## Changes Detected\n\n"); - for (final var diff : differences.values()) { + for (final var diff : differences.elements()) { final var diffObj = (JsonObject) diff; - final var status = ((JsonString) diffObj.members().get("status")).value(); + final var status = ((JsonString) diffObj.members().get("status")).string(); if (!"DIFFERENT".equals(status)) continue; - final var className = ((JsonString) diffObj.members().get("className")).value(); + final var className = ((JsonString) diffObj.members().get("className")).string(); sb.append("### ").append(className).append("\n\n"); final var classDiffs = (JsonArray) diffObj.members().get("differences"); if (classDiffs != null) { - for (final var change : classDiffs.values()) { + for (final var change : classDiffs.elements()) { final var changeObj = (JsonObject) change; - final var type = ((JsonString) changeObj.members().get("type")).value(); + final var type = ((JsonString) changeObj.members().get("type")).string(); final var methodValue = changeObj.members().get("method"); - final var method = methodValue instanceof JsonString js ? js.value() : "unknown"; + final var method = methodValue instanceof JsonString js ? js.string() : "unknown"; final var emoji = switch (type) { case "methodRemoved" -> "➖"; @@ -1078,7 +1078,7 @@ static String generateSummary(JsonObject report) { } sb.append("---\n"); - final var timestamp = ((JsonString) report.members().get("timestamp")).value(); + final var timestamp = ((JsonString) report.members().get("timestamp")).string(); sb.append("*Generated by API Tracker on ").append(timestamp.split("T")[0]).append("*\n"); return sb.toString(); 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 index c971395..3a7da94 100644 --- 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 @@ -71,15 +71,15 @@ void testExtractLocalApiJsonObject() { // Check if extraction succeeded or failed if (api.members().containsKey("error")) { // If file not found, that's expected for some source setups - final var error = ((JsonString) api.members().get("error")).value(); + final var error = ((JsonString) api.members().get("error")).string(); assertThat(error).contains("LOCAL_FILE_NOT_FOUND"); } else { // If extraction succeeded, validate structure assertThat(api.members()).containsKey("className"); - assertThat(((JsonString) api.members().get("className")).value()).isEqualTo("JsonObject"); + assertThat(((JsonString) api.members().get("className")).string()).isEqualTo("JsonObject"); assertThat(api.members()).containsKey("packageName"); - assertThat(((JsonString) api.members().get("packageName")).value()).isEqualTo("jdk.sandbox.java.util.json"); + assertThat(((JsonString) api.members().get("packageName")).string()).isEqualTo("jdk.sandbox.java.util.json"); assertThat(api.members()).containsKey("isInterface"); assertThat(api.members().get("isInterface")).isEqualTo(JsonBoolean.of(true)); @@ -94,7 +94,7 @@ void testExtractLocalApiJsonValue() { // Check if extraction succeeded or failed if (api.members().containsKey("error")) { // If file not found, that's expected for some source setups - final var error = ((JsonString) api.members().get("error")).value(); + final var error = ((JsonString) api.members().get("error")).string(); assertThat(error).contains("LOCAL_FILE_NOT_FOUND"); } else { // If extraction succeeded, validate structure @@ -114,7 +114,7 @@ void testExtractLocalApiMissingFile() { final var api = ApiTracker.extractLocalApiFromSource("jdk.sandbox.java.util.json.NonExistentClass"); assertThat(api.members()).containsKey("error"); - final var error = ((JsonString) api.members().get("error")).value(); + final var error = ((JsonString) api.members().get("error")).string(); assertThat(error).contains("LOCAL_FILE_NOT_FOUND"); } } @@ -179,7 +179,7 @@ void testCompareApisUpstreamError() { final var result = ApiTracker.compareApis(local, upstream); assertThat(result.members()).containsKey("status"); - assertThat(((JsonString) result.members().get("status")).value()).isEqualTo("UPSTREAM_ERROR"); + assertThat(((JsonString) result.members().get("status")).string()).isEqualTo("UPSTREAM_ERROR"); assertThat(result.members()).containsKey("error"); } } diff --git a/json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java b/json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java index 9470efd..4c0faf1 100644 --- a/json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java +++ b/json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java @@ -175,7 +175,7 @@ void pushChildFrames(Frame frame, java.util.Deque stack) { case JtdSchema.ElementsSchema elementsSchema -> { if (instance instanceof JsonArray arr) { int index = 0; - for (JsonValue element : arr.values()) { + for (JsonValue element : arr.elements()) { String childPtr = frame.ptr() + "/" + index; Crumbs childCrumbs = frame.crumbs().withArrayIndex(index); Frame childFrame = new Frame(elementsSchema.elements(), element, childPtr, childCrumbs); @@ -249,7 +249,7 @@ void pushChildFrames(Frame frame, java.util.Deque stack) { if (instance instanceof JsonObject obj) { JsonValue discriminatorValue = obj.members().get(discSchema.discriminator()); if (discriminatorValue instanceof JsonString discStr) { - String discriminatorValueStr = discStr.value(); + String discriminatorValueStr = discStr.string(); JtdSchema variantSchema = discSchema.mapping().get(discriminatorValueStr); if (variantSchema != null) { @@ -418,7 +418,7 @@ JtdSchema compileObjectSchema(JsonObject obj, boolean isRoot) { nullableValue.getClass().getSimpleName() + " in schema: " + Json.toDisplayString(obj, 0)); } // If nullable is valid, this becomes a nullable empty schema - if (bool.value()) { + if (bool.bool()) { return new JtdSchema.NullableSchema(new JtdSchema.EmptySchema()); } } @@ -461,7 +461,7 @@ JtdSchema compileObjectSchema(JsonObject obj, boolean isRoot) { throw new IllegalArgumentException("nullable must be a boolean, found: " + nullableValue.getClass().getSimpleName() + " in schema: " + Json.toDisplayString(obj, 0)); } - if (bool.value()) { + if (bool.bool()) { return new JtdSchema.NullableSchema(schema); } } @@ -474,7 +474,7 @@ JtdSchema compileRefSchema(JsonObject obj) { if (!(refValue instanceof JsonString str)) { throw new IllegalArgumentException("ref must be a string"); } - String ref = str.value(); + String ref = str.string(); // RFC 8927: Validate that ref points to an existing definition at compile time if (!definitions.containsKey(ref)) { @@ -501,7 +501,7 @@ JtdSchema compileTypeSchema(JsonObject obj) { throw new IllegalArgumentException("type must be a string"); } - String typeStr = str.value(); + String typeStr = str.string(); // RFC 8927 §2.2.3: Validate that type is one of the supported primitive types if (!VALID_TYPES.contains(typeStr)) { @@ -520,11 +520,11 @@ JtdSchema compileEnumSchema(JsonObject obj) { } List values = new ArrayList<>(); - for (JsonValue value : arr.values()) { + for (JsonValue value : arr.elements()) { if (!(value instanceof JsonString str)) { throw new IllegalArgumentException("enum values must be strings"); } - values.add(str.value()); + values.add(str.string()); } if (values.isEmpty()) { @@ -596,7 +596,7 @@ JtdSchema compilePropertiesSchema(JsonObject obj, boolean isRoot) { if (!(addPropsValue instanceof JsonBoolean bool)) { throw new IllegalArgumentException("additionalProperties must be a boolean"); } - additionalProperties = bool.value(); + additionalProperties = bool.bool(); } // Empty schema with no properties defined rejects additional properties by default return new JtdSchema.PropertiesSchema(properties, optionalProperties, additionalProperties); @@ -623,7 +623,7 @@ JtdSchema compileDiscriminatorSchema(JsonObject obj, boolean isRoot) { if (!(discriminatorValue instanceof JsonString discStr)) { throw new IllegalArgumentException("discriminator must be a string"); } - String discriminatorKey = discStr.value(); + String discriminatorKey = discStr.string(); JsonValue mappingValue = members.get("mapping"); if (!(mappingValue instanceof JsonObject mappingObj)) { @@ -644,7 +644,7 @@ JtdSchema compileDiscriminatorSchema(JsonObject obj, boolean isRoot) { // Check for nullable flag before compiling if (variantObj.members().containsKey("nullable") && variantObj.members().get("nullable") instanceof JsonBoolean bool && - bool.value()) { + bool.bool()) { throw new IllegalArgumentException("Discriminator mapping '" + key + "' cannot be nullable"); } diff --git a/json-java21-jtd/src/main/java/json/java21/jtd/JtdSchema.java b/json-java21-jtd/src/main/java/json/java21/jtd/JtdSchema.java index d3773b7..402ee98 100644 --- a/json-java21-jtd/src/main/java/json/java21/jtd/JtdSchema.java +++ b/json-java21-jtd/src/main/java/json/java21/jtd/JtdSchema.java @@ -148,7 +148,7 @@ boolean validateStringWithFrame(Frame frame, java.util.List errors, bool boolean validateTimestampWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); if (instance instanceof JsonString str) { - String value = str.value(); + String value = str.string(); if (RFC3339.matcher(value).matches()) { try { // Replace :60 with :59 to allow leap seconds through parsing @@ -180,29 +180,28 @@ private boolean hasFractionalComponent(Number value) { boolean validateIntegerWithFrame(Frame frame, String type, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); if (instance instanceof JsonNumber num) { - Number value = num.toNumber(); + // Check for fractional component using toDouble first + double doubleValue = num.toDouble(); - // Check for fractional component first (applies to all Number types) - if (hasFractionalComponent(value)) { + // Check for fractional component + if (doubleValue != Math.floor(doubleValue)) { String error = Jtd.Error.EXPECTED_INTEGER.message(); errors.add(Jtd.enrichedError(error, frame, instance)); return false; } - // Handle precision loss for large Double values - if (value instanceof Double d) { - if (d > Long.MAX_VALUE || d < Long.MIN_VALUE) { - String error = verboseErrors - ? Jtd.Error.EXPECTED_NUMERIC_TYPE.message(instance, type, "out of range") - : Jtd.Error.EXPECTED_NUMERIC_TYPE.message(type, "out of range"); - errors.add(Jtd.enrichedError(error, frame, instance)); - return false; - } + // Handle precision loss for large values + if (doubleValue > Long.MAX_VALUE || doubleValue < Long.MIN_VALUE) { + String error = verboseErrors + ? Jtd.Error.EXPECTED_NUMERIC_TYPE.message(instance, type, "out of range") + : Jtd.Error.EXPECTED_NUMERIC_TYPE.message(type, "out of range"); + errors.add(Jtd.enrichedError(error, frame, instance)); + return false; } // Now check if the value is within range for the specific integer type - // Convert to long for range checking (works for all Number types) - long longValue = value.longValue(); + // Convert to long for range checking + long longValue = num.toLong(); boolean inRange = switch (type) { case "int8" -> longValue >= -128 && longValue <= 127; case "uint8" -> longValue >= 0 && longValue <= 255; @@ -250,12 +249,12 @@ record EnumSchema(List values) implements JtdSchema { public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); if (instance instanceof JsonString str) { - if (values.contains(str.value())) { + if (values.contains(str.string())) { return true; } String error = verboseErrors - ? Jtd.Error.VALUE_NOT_IN_ENUM.message(instance, str.value(), values) - : Jtd.Error.VALUE_NOT_IN_ENUM.message(str.value(), values); + ? Jtd.Error.VALUE_NOT_IN_ENUM.message(instance, str.string(), values) + : Jtd.Error.VALUE_NOT_IN_ENUM.message(str.string(), values); errors.add(Jtd.enrichedError(error, frame, instance)); return false; } @@ -281,7 +280,7 @@ public Jtd.Result validate(JsonValue instance) { @Override public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { if (instance instanceof JsonArray arr) { - for (JsonValue element : arr.values()) { + for (JsonValue element : arr.elements()) { Jtd.Result result = elements.validate(element, verboseErrors); if (!result.isValid()) { return result; @@ -483,7 +482,7 @@ public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { return Jtd.Result.failure(error); } - String discriminatorValueStr = discStr.value(); + String discriminatorValueStr = discStr.string(); JtdSchema variantSchema = mapping.get(discriminatorValueStr); if (variantSchema == null) { String error = verboseErrors @@ -526,7 +525,7 @@ public boolean validateWithFrame(Frame frame, java.util.List errors, boo return false; } - String discriminatorValueStr = discStr.value(); + String discriminatorValueStr = discStr.string(); JtdSchema variantSchema = mapping.get(discriminatorValueStr); if (variantSchema == null) { String error = verboseErrors diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/JtdPropertyTest.java b/json-java21-jtd/src/test/java/json/java21/jtd/JtdPropertyTest.java index ea3ea39..e6ee61f 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/JtdPropertyTest.java +++ b/json-java21-jtd/src/test/java/json/java21/jtd/JtdPropertyTest.java @@ -98,7 +98,7 @@ private static JsonValue buildCompliantTypeValue(String type) { case "uint16" -> JsonNumber.of(50000); case "int32" -> JsonNumber.of(1000000); case "uint32" -> JsonNumber.of(3000000000L); - case "float32", "float64" -> JsonNumber.of(new BigDecimal("3.14159")); + case "float32", "float64" -> JsonNumber.of("3.14159"); default -> JsonString.of("unknown-type-value"); }; } @@ -110,10 +110,10 @@ private static List createFailingJtdDocuments(JtdTestSchema schema, J case TypeSchema(var type) -> createFailingTypeValues(type); case EnumSchema(var ignored) -> List.of(JsonString.of("invalid-enum-value")); case ElementsSchema(var elementSchema) -> { - if (compliant instanceof JsonArray arr && !arr.values().isEmpty()) { - final var invalidElement = createFailingJtdDocuments(elementSchema, arr.values().getFirst()); + if (compliant instanceof JsonArray arr && !arr.elements().isEmpty()) { + final var invalidElement = createFailingJtdDocuments(elementSchema, arr.elements().getFirst()); if (!invalidElement.isEmpty()) { - final var mixedArray = JsonArray.of(List.of(arr.values().getFirst(), invalidElement.getFirst())); + final var mixedArray = JsonArray.of(List.of(arr.elements().getFirst(), invalidElement.getFirst())); yield List.of(mixedArray, JsonNull.of()); } } @@ -153,7 +153,7 @@ private static List createFailingTypeValues(String type) { case "boolean" -> List.of(JsonString.of("not-boolean"), JsonNumber.of(1)); case "string", "timestamp" -> List.of(JsonNumber.of(123), JsonBoolean.of(false)); case "int8", "uint8", "int16", "int32", "uint32", "uint16" -> - List.of(JsonString.of("not-integer"), JsonNumber.of(new BigDecimal("3.14"))); + List.of(JsonString.of("not-integer"), JsonNumber.of("3.14")); case "float32", "float64" -> List.of(JsonString.of("not-float"), JsonBoolean.of(true)); default -> List.of(JsonNull.of()); }; diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927.java b/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927.java index 10e7491..ae9d8b5 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927.java +++ b/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927.java @@ -447,7 +447,7 @@ public void testRefSchemaRecursiveBad() { @Test public void testInt32RejectsDecimal() { JsonValue schema = Json.parse("{\"type\": \"int32\"}"); - JsonValue decimalValue = JsonNumber.of(new java.math.BigDecimal("3.14")); + JsonValue decimalValue = JsonNumber.of("3.14"); LOG.info(() -> "Testing int32 validation against decimal value 3.14"); LOG.fine(() -> "Schema: " + schema); @@ -479,10 +479,10 @@ public void testIntegerTypesAcceptTrailingZeros() { // Valid integer representations with trailing zeros JsonValue[] validIntegers = { - JsonNumber.of(new java.math.BigDecimal("3.0")), - JsonNumber.of(new java.math.BigDecimal("3.000")), - JsonNumber.of(new java.math.BigDecimal("42.00")), - JsonNumber.of(new java.math.BigDecimal("0.0")) + JsonNumber.of("3.0"), + JsonNumber.of("3.000"), + JsonNumber.of("42.00"), + JsonNumber.of("0.0") }; Jtd validator = new Jtd(); @@ -509,10 +509,10 @@ public void testIntegerTypesRejectFractionalComponents() { // Invalid values with actual fractional components JsonValue[] invalidValues = { - JsonNumber.of(new java.math.BigDecimal("3.1")), - JsonNumber.of(new java.math.BigDecimal("3.0001")), - JsonNumber.of(new java.math.BigDecimal("3.14")), - JsonNumber.of(new java.math.BigDecimal("0.1")) + JsonNumber.of("3.1"), + JsonNumber.of("3.0001"), + JsonNumber.of("3.14"), + JsonNumber.of("0.1") }; Jtd validator = new Jtd(); @@ -900,8 +900,8 @@ public void testInt8RangeValidationWithDoubleValues() { Jtd.Result result = validator.validate(schema, outOfRange); LOG.fine(() -> "Testing int8 range with Double value: " + outOfRange + - " (JsonNumber.toNumber() type: " + - ((JsonNumber)outOfRange).toNumber().getClass().getSimpleName() + ")"); + " (JsonNumber.toLong(): " + + ((JsonNumber)outOfRange).toLong() + ")"); // This should fail but currently passes due to the bug assertThat(result.isValid()) @@ -1041,16 +1041,15 @@ public void testUint32RangeValidationWithDoubleValues() { public void testJsonNumberToNumberReturnsDouble() { JsonValue numberValue = Json.parse("1000"); - // Verify that JsonNumber.toNumber() returns Double for typical JSON numbers + // Verify that JsonNumber works properly for typical JSON numbers assertThat(numberValue).isInstanceOf(JsonNumber.class); - Number number = ((JsonNumber) numberValue).toNumber(); + long longValue = ((JsonNumber) numberValue).toLong(); - LOG.info(() -> "JsonNumber.toNumber() returns: " + number.getClass().getSimpleName() + + LOG.info(() -> "JsonNumber.toLong() returns: " + longValue + " for value: " + numberValue); - // This demonstrates what type JsonNumber.toNumber() returns for typical values - // The actual type depends on the JSON parser implementation - LOG.info(() -> "JsonNumber.toNumber() returns: " + number.getClass().getSimpleName() + + // This demonstrates what value JsonNumber.toLong() returns for typical values + LOG.info(() -> "JsonNumber.toLong() returns: " + longValue + " for value: " + numberValue); // The key test is that regardless of the Number type, range validation should work @@ -1070,7 +1069,7 @@ public void testIntegerValidationExplicitDouble() { Jtd.Result result = validator.validate(schema, doubleValue); LOG.fine(() -> "Explicit Double validation - value: " + doubleValue + - ", toNumber() type: " + doubleValue.toNumber().getClass().getSimpleName()); + ", toDouble(): " + doubleValue.toDouble()); // This should fail (1000 is way outside int8 range of -128 to 127) assertThat(result.isValid()) @@ -1107,7 +1106,7 @@ public void testIntegerBoundaryValues() { @Test public void testIntegerValidationWithBigDecimalValues() { JsonValue schema = Json.parse("{\"type\": \"uint32\"}"); - JsonValue bigValue = JsonNumber.of(new java.math.BigDecimal("5000000000")); // > uint32 max + JsonValue bigValue = JsonNumber.of("5000000000"); // > uint32 max Jtd validator = new Jtd(); Jtd.Result result = validator.validate(schema, bigValue); diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java index 66d4f92..43b8614 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -27,8 +27,10 @@ import java.util.Collections; import java.util.List; + import jdk.sandbox.java.util.json.JsonArray; import jdk.sandbox.java.util.json.JsonValue; + /** * JsonArray implementation class */ @@ -49,7 +51,7 @@ public JsonArrayImpl(List from, int o, char[] d) { } @Override - public List values() { + public List elements() { return Collections.unmodifiableList(theValues); } @@ -66,10 +68,10 @@ public int offset() { @Override public String toString() { var s = new StringBuilder("["); - for (JsonValue v: values()) { + for (JsonValue v: elements()) { s.append(v.toString()).append(","); } - if (!values().isEmpty()) { + if (!elements().isEmpty()) { s.setLength(s.length() - 1); // trim final comma } return s.append("]").toString(); @@ -78,11 +80,11 @@ public String toString() { @Override public boolean equals(Object o) { return o instanceof JsonArray oja && - values().equals(oja.values()); + elements().equals(oja.elements()); } @Override public int hashCode() { - return values().hashCode(); + return elements().hashCode(); } } diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java index 476375c..cb9d82c 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java @@ -26,6 +26,7 @@ package jdk.sandbox.internal.util.json; import jdk.sandbox.java.util.json.JsonBoolean; + /** * JsonBoolean implementation class */ @@ -45,7 +46,7 @@ public JsonBooleanImpl(Boolean bool, char[] doc, int offset) { } @Override - public boolean value() { + public boolean bool() { return theBoolean; } @@ -61,16 +62,16 @@ public int offset() { @Override public String toString() { - return String.valueOf(value()); + return String.valueOf(bool()); } @Override public boolean equals(Object o) { - return o instanceof JsonBoolean ojb && value() == ojb.value(); + return o instanceof JsonBoolean ojb && bool() == ojb.bool(); } @Override public int hashCode() { - return Boolean.hashCode(value()); + return Boolean.hashCode(bool()); } } diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java index f8852af..f3f308c 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java @@ -26,6 +26,7 @@ package jdk.sandbox.internal.util.json; import jdk.sandbox.java.util.json.JsonNull; + /** * JsonNull implementation class */ diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java index 719cb27..d48003a 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java @@ -25,10 +25,12 @@ package jdk.sandbox.internal.util.json; -import java.math.BigDecimal; -import java.math.BigInteger; import java.util.Locale; + +import java.util.Optional; import jdk.sandbox.java.util.json.JsonNumber; + + /** * JsonNumber implementation class */ @@ -37,64 +39,31 @@ public final class JsonNumberImpl implements JsonNumber, JsonValueImpl { private final char[] doc; private final int startOffset; private final int endOffset; - private final boolean isFp; - private final StableValue theNumber = StableValue.of(); - private final StableValue numString = StableValue.of(); - private final StableValue cachedBD = StableValue.of(); - - public JsonNumberImpl(Number num) { - // Called by factories. Input is Double, Long, BI, or BD. - if (num == null || - num instanceof Double d && (d.isNaN() || d.isInfinite())) { - throw new IllegalArgumentException("Not a valid JSON number"); - } - theNumber.setOrThrow(num); - numString.setOrThrow(num.toString()); - // unused - startOffset = -1; - endOffset = -1; - isFp = false; - doc = null; - } + private final int decimalOffset; + private final int exponentOffset; + + private final LazyConstant numString = LazyConstant.of(this::initNumString); + private final LazyConstant> numLong = LazyConstant.of(this::initNumLong); + private final LazyConstant> numDouble = LazyConstant.of(this::initNumDouble); - public JsonNumberImpl(char[] doc, int start, int end, boolean fp) { + public JsonNumberImpl(char[] doc, int start, int end, int dec, int exp) { this.doc = doc; startOffset = start; endOffset = end; - isFp = fp; + decimalOffset = dec; + exponentOffset = exp; } @Override - public Number toNumber() { - return theNumber.orElseSet(() -> { - var str = toString(); - if (isFp) { - var db = Double.parseDouble(str); - if (Double.isInfinite(db)) { - return toBigDecimal(); - } else { - return db; - } - } else { - try { - return Long.parseLong(str); - } catch(NumberFormatException e) { - return new BigInteger(str); - } - } - }); + public long toLong() { + return numLong.get().orElseThrow(() -> + Utils.composeError(this, this + " cannot be represented as a long.")); } @Override - public BigDecimal toBigDecimal() { - return cachedBD.orElseSet(() -> { - // If we already computed theNumber, check if it's BD - if (theNumber.orElse(null) instanceof BigDecimal bd) { - return bd; - } else { - return new BigDecimal(toString()); - } - }); + public double toDouble() { + return numDouble.get().orElseThrow(() -> + Utils.composeError(this, this + " cannot be represented as a double.")); } @Override @@ -109,8 +78,7 @@ public int offset() { @Override public String toString() { - return numString.orElseSet( - () -> new String(doc, startOffset, endOffset - startOffset)); + return numString.get(); } @Override @@ -123,4 +91,71 @@ public boolean equals(Object o) { public int hashCode() { return toString().toLowerCase(Locale.ROOT).hashCode(); } + + // LazyConstants initializers + private String initNumString() { + return new String(doc, startOffset, endOffset - startOffset); + } + + // 4 cases: Fully integral, has decimal, has exponent, has decimal and exponent + private Optional initNumLong() { + try { + if (decimalOffset == -1 && exponentOffset == -1) { + // Parseable Long format + return Optional.of(Long.parseLong(numString.get())); + } else { + // Decimal or exponent exists, can't parse w/ Long::parseLong + if (exponentOffset != -1) { + // Exponent exists + // Calculate exponent value + int exp = Math.abs(Integer.parseInt(new String(doc, + exponentOffset + 1, endOffset - exponentOffset - 1), 10)); + long sig; + long scale; + if (decimalOffset == -1) { + // Exponent with no decimal + sig = Long.parseLong(new String(doc, startOffset, exponentOffset - startOffset)); + } else { + // Exponent with decimal + for (int i = decimalOffset + exp + 1; i < exponentOffset; i++) { + if (doc[i] != '0') { + return Optional.empty(); + } + } + var shiftedFractionPart = new String(doc, decimalOffset + 1, Math.min(exp, exponentOffset - decimalOffset - 1)); + exp = exp - shiftedFractionPart.length(); + sig = Long.parseLong(new String(doc, startOffset, decimalOffset - startOffset) + shiftedFractionPart); + } + scale = Utils.powExact(10L, exp); + if (doc[exponentOffset + 1] != '-') { + return Optional.of(Math.multiplyExact(sig, scale)); + } else { + if (sig % scale == 0) { + return Optional.of(Math.divideExact(sig, scale)); + } else { + return Optional.empty(); + } + } + } else { + // Decimal with no exponent + for (int i = decimalOffset + 1; i < endOffset; i++) { + if (doc[i] != '0') { + return Optional.empty(); + } + } + return Optional.of(Long.parseLong(new String(doc, + startOffset, decimalOffset - startOffset), 10)); + } + } + } catch (NumberFormatException | ArithmeticException ignored) {} + return Optional.empty(); + } + + private Optional initNumDouble() { + var db = Double.parseDouble(numString.get()); + if (Double.isFinite(db)) { + return Optional.of(db); + } + return Optional.empty(); + } } diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java index 5a232b6..8d6e8de 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -27,8 +27,10 @@ import java.util.Collections; import java.util.Map; + import jdk.sandbox.java.util.json.JsonObject; import jdk.sandbox.java.util.json.JsonValue; + /** * JsonObject implementation class */ diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java index fb9a95c..b5261f3 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.function.Supplier; + import jdk.sandbox.java.util.json.JsonArray; import jdk.sandbox.java.util.json.JsonObject; import jdk.sandbox.java.util.json.JsonParseException; @@ -40,15 +41,14 @@ * Parses a JSON Document char[] into a tree of JsonValues. JsonObject and JsonArray * nodes create their data structures which maintain the connection to children. * JsonNumber and JsonString contain only a start and end offset, which - * are used to lazily procure their underlying value/string on demand. Singletons - * are used for JsonBoolean and JsonNull. + * are used to lazily procure their underlying value/string on demand. */ public final class JsonParser { // Access to the underlying JSON contents private final char[] doc; // Lazily initialized for member names with escape sequences - private final Supplier sb = StableValue.supplier(this::initSb); + private final LazyConstant sb = LazyConstant.of(this::initSb); // Current offset during parsing private int offset; // For exception message on failure @@ -114,10 +114,7 @@ private JsonObject parseObject() { // Why not parse the name as a JsonString and then return its value()? // Would requires 2 passes; we should build the String as we parse. var name = parseName(); - - if (members.containsKey(name)) { - throw failure("The duplicate member name: \"%s\" was already parsed".formatted(name)); - } + var nameOffset = offset; // Move from name to ':' skipWhitespaces(); @@ -125,7 +122,11 @@ private JsonObject parseObject() { throw failure( "Expected a colon after the member name"); } - members.put(name, parseValue()); + + if (members.putIfAbsent(name, parseValue()) != null) { + throw failure(nameOffset, "The duplicate member name: \"%s\" was already parsed".formatted(name)); + } + // Ensure current char is either ',' or '}' if (charEquals('}')) { return new JsonObjectImpl(members, startO, doc); @@ -181,7 +182,7 @@ private String parseName() { } else if (c == '\"') { offset++; if (useBldr) { - var name = sb.toString(); + var name = sb.get().toString(); sb.get().setLength(0); return name; } else { @@ -257,31 +258,27 @@ private JsonString parseString() { throw failure(UNCLOSED_STRING.formatted("JSON String")); } - /* - * Parsing true, false, and null return singletons. These JsonValues - * do not require offsets to lazily compute their values. - */ private JsonBooleanImpl parseTrue() { - offset++; + var start = offset++; if (charEquals('r') && charEquals('u') && charEquals('e')) { - return new JsonBooleanImpl(true, doc, offset); + return new JsonBooleanImpl(true, doc, start); } throw failure(UNEXPECTED_VAL); } private JsonBooleanImpl parseFalse() { - offset++; + var start = offset++; if (charEquals('a') && charEquals('l') && charEquals('s') && charEquals('e')) { - return new JsonBooleanImpl(false, doc, offset); + return new JsonBooleanImpl(false, doc, start); } throw failure(UNEXPECTED_VAL); } private JsonNullImpl parseNull() { - offset++; + var start = offset++; if (charEquals('u') && charEquals('l') && charEquals('l')) { - return new JsonNullImpl(doc, offset); + return new JsonNullImpl(doc, start); } throw failure(UNEXPECTED_VAL); } @@ -293,8 +290,8 @@ private JsonNullImpl parseNull() { * See https://datatracker.ietf.org/doc/html/rfc8259#section-6 */ private JsonNumberImpl parseNumber() { - boolean sawDecimal = false; - boolean sawExponent = false; + int decOff = -1; + int expOff = -1; boolean sawZero = false; boolean havePart = false; boolean sawSign = false; @@ -305,19 +302,19 @@ private JsonNumberImpl parseNumber() { var c = doc[offset]; switch (c) { case '-' -> { - if (offset != start && !sawExponent || sawSign) { + if (offset != start && expOff == -1 || sawSign) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } sawSign = true; } case '+' -> { - if (!sawExponent || havePart || sawSign) { + if (expOff == -1 || havePart || sawSign) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } sawSign = true; } case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> { - if (!sawDecimal && !sawExponent && sawZero) { + if (decOff == -1 && expOff == -1 && sawZero) { throw failure(INVALID_POSITION_IN_NUMBER.formatted('0')); } if (doc[offset] == '0' && !havePart) { @@ -326,24 +323,24 @@ private JsonNumberImpl parseNumber() { havePart = true; } case '.' -> { - if (sawDecimal) { + if (decOff != -1) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } else { if (!havePart) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } - sawDecimal = true; + decOff = offset; havePart = false; } } case 'e', 'E' -> { - if (sawExponent) { + if (expOff != -1) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } else { if (!havePart) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } - sawExponent = true; + expOff = offset; havePart = false; sawSign = false; } @@ -357,7 +354,7 @@ private JsonNumberImpl parseNumber() { if (!havePart) { throw failure("Input expected after '[.|e|E]'"); } - return new JsonNumberImpl(doc, start, offset, sawDecimal || sawExponent); + return new JsonNumberImpl(doc, start, offset, decOff, expOff); } // Utility functions @@ -408,7 +405,7 @@ private boolean notWhitespace() { return switch (doc[offset]) { case ' ', '\t','\r' -> false; case '\n' -> { - // Increments the row and col + // Increments the line and lineStart line++; lineStart = offset + 1; yield false; @@ -427,15 +424,15 @@ private boolean charEquals(char c) { return false; } - // Return the col position reflective of the current row - private int col() { - return offset - lineStart; + private JsonParseException failure(String message) { + return failure(offset, message); } - private JsonParseException failure(String message) { + private JsonParseException failure(int off, String message) { // Non-revealing message does not produce input source String - return new JsonParseException("%s. Location: row %d, col %d." - .formatted(message, line, col()), line, col()); + var pos = off - lineStart; + return new JsonParseException("%s. Location: line %d, position %d." + .formatted(message, line, pos), line, pos); } // Parsing error messages ---------------------- diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java index f73b899..1a9f076 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java @@ -26,6 +26,7 @@ package jdk.sandbox.internal.util.json; import jdk.sandbox.java.util.json.JsonString; + /** * JsonString implementation class */ @@ -40,22 +41,14 @@ public final class JsonStringImpl implements JsonString, JsonValueImpl { // It always conforms to JSON syntax. If created by parsing a JSON document, // it matches the original text exactly. If created via the factory method, // non-conformant characters are properly escaped. - private final StableValue jsonStr = StableValue.of(); - - // The String instance returned by `value()`. - // If created by parsing a JSON document, escaped characters are unescaped. - // If created via the factory method, the input String is used as-is. - private final StableValue value = StableValue.of(); - - // Called by JsonString.of() factory. The passed String represents the - // unescaped value. - public JsonStringImpl(String str) { - jsonStr.setOrThrow('"' + Utils.escape(value.orElseSet(() -> str)) + '"'); - // unused - doc = null; - startOffset = -1; - endOffset = -1; - hasEscape = false; + private final LazyConstant jsonStr = LazyConstant.of(this::initJsonStr); + + // The String instance returned by `string()`. Escaped characters are unescaped. + private final LazyConstant value = LazyConstant.of(this::unescape); + + // LazyConstants initializers + private String initJsonStr() { + return new String(doc, startOffset, endOffset - startOffset); } public JsonStringImpl(char[] doc, int start, int end, boolean escape) { @@ -66,8 +59,8 @@ public JsonStringImpl(char[] doc, int start, int end, boolean escape) { } @Override - public String value() { - return value.orElseSet(this::unescape); + public String string() { + return value.get(); } @Override @@ -82,8 +75,7 @@ public int offset() { @Override public String toString() { - return jsonStr.orElseSet( - () -> new String(doc, startOffset, endOffset - startOffset)); + return jsonStr.get(); } /* @@ -130,11 +122,11 @@ private String unescape() { @Override public boolean equals(Object o) { return o instanceof JsonString ojs && - value().equals(ojs.value()); + string().equals(ojs.string()); } @Override public int hashCode() { - return value().hashCode(); + return string().hashCode(); } } diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonValueImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonValueImpl.java index 6d595e8..a082733 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonValueImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonValueImpl.java @@ -1,8 +1,20 @@ package jdk.sandbox.internal.util.json; -// Minimal internal marker for backport compatibility -interface JsonValueImpl { +/** + * Used for JsonAssertionException error message building. + */ +public sealed interface JsonValueImpl + permits JsonArrayImpl, JsonBooleanImpl, JsonNullImpl, JsonNumberImpl, JsonObjectImpl, JsonStringImpl { + + /** + * Return access to the underlying document, if it was parsed. + * Otherwise, return null. + */ char[] doc(); + + /** + * Return the associated offset of the JsonValue in the document, if it was parsed. + * Otherwise, return -1. + */ int offset(); } - diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/LazyConstant.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/LazyConstant.java new file mode 100644 index 0000000..71bdff9 --- /dev/null +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/LazyConstant.java @@ -0,0 +1,36 @@ +package jdk.sandbox.internal.util.json; + +import java.util.function.Supplier; + +/// Polyfill for JDK's LazyConstant using double-checked locking pattern +/// for thread-safe lazy initialization. +/// +/// This provides a simpler API than StableValue: +/// - `LazyConstant.of(Supplier)` - creates a lazy constant +/// - `.get()` - gets the value (computing if needed) +class LazyConstant { + private volatile T value; + private final Supplier supplier; + private final Object lock = new Object(); + + private LazyConstant(Supplier supplier) { + this.supplier = supplier; + } + + public static LazyConstant of(Supplier supplier) { + return new LazyConstant<>(supplier); + } + + public T get() { + T result = value; + if (result == null) { + synchronized (lock) { + result = value; + if (result == null) { + value = result = supplier.get(); + } + } + } + return result; + } +} diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java index 0157803..0258f7d 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java @@ -25,8 +25,6 @@ package jdk.sandbox.internal.util.json; -import java.util.List; -import java.util.Map; import jdk.sandbox.java.util.json.JsonArray; import jdk.sandbox.java.util.json.JsonAssertionException; import jdk.sandbox.java.util.json.JsonBoolean; @@ -44,16 +42,6 @@ public class Utils { // Non instantiable private Utils() {} - // Equivalent to JsonObject/Array.of() factories without the need for defensive copy - // and other input validation - public static JsonArray arrayOf(List list) { - return new JsonArrayImpl(list); - } - - public static JsonObject objectOf(Map map) { - return new JsonObjectImpl(map); - } - /* * Escapes a String to ensure it is a valid JSON String. * Backslash, double quote, and control chars are escaped. @@ -99,31 +87,33 @@ public static String escape(String str) { return sb == null ? str : sb.toString(); } + public static JsonAssertionException composeError(JsonValue jv, String message) { + return new JsonAssertionException(message + + (jv instanceof JsonValueImpl jvi && jvi.doc() != null ? JsonPath.getPath(jvi) : "")); + } + // Use to compose an exception when casting to an incorrect type public static JsonAssertionException composeTypeError(JsonValue jv, String expected) { var actual = switch (jv) { - case JsonObject obj -> "JsonObject"; - case JsonArray arr -> "JsonArray"; - case JsonBoolean b -> "JsonBoolean"; - case JsonNull n -> "JsonNull"; - case JsonNumber num -> "JsonNumber"; - case JsonString str -> "JsonString"; + case JsonObject ignored -> "JsonObject"; + case JsonArray ignored -> "JsonArray"; + case JsonBoolean ignored -> "JsonBoolean"; + case JsonNull ignored -> "JsonNull"; + case JsonNumber ignored -> "JsonNumber"; + case JsonString ignored -> "JsonString"; }; - return new JsonAssertionException("%s is not a %s.".formatted(actual, expected) - + ((jv instanceof JsonValueImpl jvi && jvi.doc() != null) ? JsonPath.getPath(jvi) : "")); - } - - static String getPath(JsonValueImpl jvi) { - return JsonPath.getPath(jvi); + return composeError(jv, "%s is not a %s.".formatted(actual, expected)); } + // This class is responsible for creating the path produced by JAE. + // Backtracks from the offset of the offending JSON element to the root. private static final class JsonPath { private final int offset; private final char[] doc; // Tracked and incremented during path creation - private int row; - private int col; + private int line; + private int pos; private JsonPath(JsonValueImpl jvi) { this.offset = jvi.offset(); @@ -138,17 +128,17 @@ private String parseToRoot() { var sb = new StringBuilder(); // Updates the sb toPath(offset, sb); - // If no new line encountered, col is the starting offset value - if (row == 0) { - col = offset; + // If no new line encountered, pos is the starting offset value + if (line == 0) { + pos = offset; } - return " Path: \"%s\". Location: row %d, col %d.".formatted(sb.toString(), row, col); + return " Path: \"%s\". Location: line %d, position %d.".formatted(sb.toString(), line, pos); } - private void addRow(int curr) { - row++; - if (row == 1) { - col = offset - curr - 1; + private void addLine(int curr) { + line++; + if (line == 1) { + pos = offset - curr - 1; } } @@ -175,7 +165,7 @@ private int walkWhitespace(int offset) { var ws = switch (doc[offset]) { case ' ', '\t','\r' -> true; case '\n' -> { - addRow(offset); + addLine(offset); yield true; } default -> false; @@ -215,7 +205,7 @@ private int arrayNode(int offset, StringBuilder sb) { } else if (c == '"') { inString = true; } else if (c == '\n') { - addRow(offset); + addLine(offset); } if (aDepth > 0) { break; @@ -271,7 +261,7 @@ private int objectNode(int offset, StringBuilder sb) { } else if (c == '"') { inString = true; } else if (c == '\n') { - addRow(offset); + addLine(offset); } if (depth > 0) { break; @@ -282,4 +272,23 @@ private int objectNode(int offset, StringBuilder sb) { return offset; } } + + // Polyfill for Math.powExact(long, int) which is not available in Java 21 + // Computes base^exponent with overflow checking + public static long powExact(long base, int exponent) { + if (exponent < 0) { + throw new ArithmeticException("Negative exponent"); + } + if (exponent == 0) { + return 1L; + } + if (exponent == 1) { + return base; + } + long result = 1L; + for (int i = 0; i < exponent; i++) { + result = Math.multiplyExact(result, base); + } + return result; + } } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java index 0594914..7d1303f 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -24,306 +24,159 @@ */ package jdk.sandbox.java.util.json; -import java.math.BigDecimal; -import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.HashMap; -import java.util.LinkedHashMap; +import java.util.stream.Collectors; import jdk.sandbox.internal.util.json.JsonParser; import jdk.sandbox.internal.util.json.Utils; -/// This class provides static methods for producing and manipulating a {@link JsonValue}. -/// -/// {@link #parse(String)} and {@link #parse(char[])} produce a `JsonValue` -/// by parsing data adhering to the JSON syntax defined in RFC 8259. -/// -/// {@link #toDisplayString(JsonValue, int)} is a formatter that produces a -/// representation of the JSON value suitable for display. -/// -/// {@link #fromUntyped(Object)} and {@link #toUntyped(JsonValue)} provide a conversion -/// between `JsonValue` and an untyped object. -/// -/// ## Example Usage -/// ```java -/// // Parse JSON string -/// JsonValue json = Json.parse("{\"name\":\"John\",\"age\":30}"); -/// -/// // Convert to standard Java types -/// Map data = (Map) Json.toUntyped(json); -/// -/// // Create JSON from Java objects -/// JsonValue fromJava = Json.fromUntyped(Map.of("active", true, "score", 95)); -/// ``` -/// -/// @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript -/// Object Notation (JSON) Data Interchange Format -/// @since 99 +/** + * This class provides static methods for parsing and generating JSON documents + * + *

+ * {@link #parse(String)} and {@link #parse(char[])} produce a {@code JsonValue} + * by parsing data adhering to the JSON syntax defined in RFC 8259. Unsuccessful + * parsing throws a {@link JsonParseException}. + *

+ * {@link #toDisplayString(JsonValue, int)} produces a + * JSON text representation of the given {@code JsonValue} suitable for display. + * + * @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript + * Object Notation (JSON) Data Interchange Format + * @since 99 + */ public final class Json { - /// Parses and creates a `JsonValue` from the given JSON document. - /// If parsing succeeds, it guarantees that the input document conforms to - /// the JSON syntax. If the document contains any JSON Object that has - /// duplicate names, a `JsonParseException` is thrown. - /// - /// `JsonValue`s created by this method produce their String and underlying - /// value representation lazily. - /// - /// `JsonObject`s preserve the order of their members declared in and parsed from - /// the JSON document. - /// - /// ## Example - /// ```java - /// JsonValue value = Json.parse("{\"name\":\"Alice\",\"active\":true}"); - /// if (value instanceof JsonObject obj) { - /// String name = ((JsonString) obj.members().get("name")).value(); - /// boolean active = ((JsonBoolean) obj.members().get("active")).value(); - /// } - /// ``` - /// - /// @param in the input JSON document as `String`. Non-null. - /// @throws JsonParseException if the input JSON document does not conform - /// to the JSON document format or a JSON object containing - /// duplicate names is encountered. - /// @throws NullPointerException if `in` is `null` - /// @return the parsed `JsonValue` + /** + * Parses and creates a {@code JsonValue} from the given JSON document. + * If parsing succeeds, it guarantees that the input document conforms to + * the JSON syntax. If the document contains any JSON object that has + * duplicate names, a {@code JsonParseException} is thrown. + *

+ * {@code JsonObject}s preserve the order of their members declared in and parsed from + * the JSON document. + * + * @implNote {@code JsonValue}s created by this method may produce their + * underlying value representation lazily. + * + * @param in the input JSON document as {@code String}. Non-null. + * @throws JsonParseException if the input JSON document does not conform + * to the JSON document format or a JSON object containing + * duplicate names is encountered. + * @throws NullPointerException if {@code in} is {@code null} + * @return the parsed {@code JsonValue} + */ public static JsonValue parse(String in) { Objects.requireNonNull(in); return new JsonParser(in.toCharArray()).parseRoot(); } - /// Parses and creates a `JsonValue` from the given JSON document. - /// If parsing succeeds, it guarantees that the input document conforms to - /// the JSON syntax. If the document contains any JSON Object that has - /// duplicate names, a `JsonParseException` is thrown. - /// - /// `JsonValue`s created by this method produce their String and underlying - /// value representation lazily. - /// - /// `JsonObject`s preserve the order of their members declared in and parsed from - /// the JSON document. - /// - /// @param in the input JSON document as `char[]`. Non-null. - /// @throws JsonParseException if the input JSON document does not conform - /// to the JSON document format or a JSON object containing - /// duplicate names is encountered. - /// @throws NullPointerException if `in` is `null` - /// @return the parsed `JsonValue` + /** + * Parses and creates a {@code JsonValue} from the given JSON document. + * If parsing succeeds, it guarantees that the input document conforms to + * the JSON syntax. If the document contains any JSON object that has + * duplicate names, a {@code JsonParseException} is thrown. + *

+ * {@code JsonObject}s preserve the order of their members declared in and parsed from + * the JSON document. + * + * @implNote {@code JsonValue}s created by this method may produce their + * underlying value representation lazily. + * + * @param in the input JSON document as {@code char[]}. Non-null. + * @throws JsonParseException if the input JSON document does not conform + * to the JSON document format or a JSON object containing + * duplicate names is encountered. + * @throws NullPointerException if {@code in} is {@code null} + * @return the parsed {@code JsonValue} + */ public static JsonValue parse(char[] in) { Objects.requireNonNull(in); // Defensive copy on input. Ensure source is immutable. return new JsonParser(Arrays.copyOf(in, in.length)).parseRoot(); } - /// {@return a `JsonValue` created from the given `src` object} - /// The mapping from an untyped `src` object to a `JsonValue` - /// follows the table below. - /// - /// | Untyped Object | JsonValue | - /// |----------------|----------| - /// | `List` | `JsonArray` | - /// | `Boolean` | `JsonBoolean` | - /// | `null` | `JsonNull` | - /// | `Number*` | `JsonNumber` | - /// | `Map` | `JsonObject` | - /// | `String` | `JsonString` | - /// - /// *The supported `Number` subclasses are: `Byte`, - /// `Short`, `Integer`, `Long`, `Float`, - /// `Double`, `BigInteger`, and `BigDecimal`. - /// - /// If `src` is an instance of `JsonValue`, it is returned as is. - /// - /// ## Example - /// ```java - /// // Convert Java collections to JSON - /// JsonValue json = Json.fromUntyped(Map.of( - /// "user", Map.of( - /// "name", "Bob", - /// "age", 25 - /// ), - /// "scores", List.of(85, 90, 78) - /// )); - /// ``` - /// - /// @param src the data to produce the `JsonValue` from. May be null. - /// @throws IllegalArgumentException if `src` cannot be converted - /// to a `JsonValue`. - /// @see #toUntyped(JsonValue) - public static JsonValue fromUntyped(Object src) { - return switch (src) { - // Structural: JSON object - case Map map -> { - Map m = LinkedHashMap.newLinkedHashMap(map.size()); - for (Map.Entry entry : map.entrySet()) { - if (!(entry.getKey() instanceof String key)) { - throw new IllegalArgumentException( - "The key '%s' is not a String".formatted(entry.getKey())); - } else { - m.put(key, Json.fromUntyped(entry.getValue())); - } - } - // Bypasses defensive copy in JsonObject.of(m) - yield Utils.objectOf(m); - } - // Structural: JSON Array - case List list -> { - List l = new ArrayList<>(list.size()); - for (Object o : list) { - l.add(Json.fromUntyped(o)); - } - // Bypasses defensive copy in JsonArray.of(l) - yield Utils.arrayOf(l); - } - // JSON primitives - case String str -> JsonString.of(str); - case Boolean bool -> JsonBoolean.of(bool); - case Byte b -> JsonNumber.of(b); - case Integer i -> JsonNumber.of(i); - case Long l -> JsonNumber.of(l); - case Short s -> JsonNumber.of(s); - case Float f -> JsonNumber.of(f); - case Double d -> JsonNumber.of(d); - case BigInteger bi -> JsonNumber.of(bi); - case BigDecimal bd -> JsonNumber.of(bd); - case null -> JsonNull.of(); - // JsonValue - case JsonValue jv -> jv; - default -> throw new IllegalArgumentException(src.getClass().getSimpleName() + " is not a recognized type"); - }; - } - - /// {@return an `Object` created from the given `src` `JsonValue`} - /// The mapping from a `JsonValue` to an untyped `src` object follows the table below. - /// - /// | JsonValue | Untyped Object | - /// |-----------|----------------| - /// | `JsonArray` | `List` (unmodifiable) | - /// | `JsonBoolean` | `Boolean` | - /// | `JsonNull` | `null` | - /// | `JsonNumber` | `Number` | - /// | `JsonObject` | `Map` (unmodifiable) | - /// | `JsonString` | `String` | - /// - /// A `JsonObject` in `src` is converted to a `Map` whose - /// entries occur in the same order as the `JsonObject`'s members. - /// - /// ## Example - /// ```java - /// JsonValue json = Json.parse("{\"active\":true,\"count\":42}"); - /// Map data = (Map) Json.toUntyped(json); - /// // data contains: {"active"=true, "count"=42L} - /// ``` - /// - /// @param src the `JsonValue` to convert to untyped. Non-null. - /// @throws NullPointerException if `src` is `null` - /// @see #fromUntyped(Object) - public static Object toUntyped(JsonValue src) { - Objects.requireNonNull(src); - return switch (src) { - case JsonObject jo -> jo.members().entrySet().stream() - .collect(LinkedHashMap::new, // Avoid Collectors.toMap, to allow `null` value - (m, e) -> m.put(e.getKey(), Json.toUntyped(e.getValue())), - HashMap::putAll); - case JsonArray ja -> ja.values().stream() - .map(Json::toUntyped) - .toList(); - case JsonBoolean jb -> jb.value(); - case JsonNull ignored -> null; - case JsonNumber n -> n.toNumber(); - case JsonString js -> js.value(); - }; - } - - /// {@return the String representation of the given `JsonValue` that conforms - /// to the JSON syntax} As opposed to the compact output returned by {@link - /// JsonValue#toString()}, this method returns a JSON string that is better - /// suited for display. - /// - /// ## Example - /// ```java - /// JsonValue json = Json.parse("{\"name\":\"Alice\",\"scores\":[85,90,95]}"); - /// System.out.println(Json.toDisplayString(json, 2)); - /// // Output: - /// // { - /// // "name": "Alice", - /// // "scores": [ - /// // 85, - /// // 90, - /// // 95 - /// // ] - /// // } - /// ``` - /// - /// @param value the `JsonValue` to create the display string from. Non-null. - /// @param indent the number of spaces used for the indentation. Zero or positive. - /// @throws NullPointerException if `value` is `null` - /// @throws IllegalArgumentException if `indent` is a negative number - /// @see JsonValue#toString() + /** + * {@return the String representation of the given {@code JsonValue} that conforms + * to the JSON syntax} As opposed to the compact output returned by {@link + * JsonValue#toString()}, this method returns a JSON string that is better + * suited for display. + * + * @param value the {@code JsonValue} to create the display string from. Non-null. + * @param indent the number of spaces used for the indentation. Zero or positive. + * @throws NullPointerException if {@code value} is {@code null} + * @throws IllegalArgumentException if {@code indent} is a negative number + * @see JsonValue#toString() + */ public static String toDisplayString(JsonValue value, int indent) { Objects.requireNonNull(value); if (indent < 0) { throw new IllegalArgumentException("indent is negative"); } - return toDisplayString(value, 0, indent, false); + var s = new StringBuilder(); + toDisplayString(value, s, 0, indent, false); + return s.toString(); } - private static String toDisplayString(JsonValue jv, int col, int indent, boolean isField) { - return switch (jv) { - case JsonObject jo -> toDisplayString(jo, col, indent, isField); - case JsonArray ja -> toDisplayString(ja, col, indent, isField); - default -> " ".repeat(isField ? 1 : col) + jv; - }; + private static void toDisplayString(JsonValue jv, StringBuilder s, int col, int indent, boolean isField) { + switch (jv) { + case JsonObject jo -> toDisplayString(jo, s, col, indent, isField); + case JsonArray ja -> toDisplayString(ja, s, col, indent, isField); + default -> s.append(" ".repeat(isField ? 1 : col)).append(jv); + } } - private static String toDisplayString(JsonObject jo, int col, int indent, boolean isField) { + private static void toDisplayString(JsonObject jo, StringBuilder s, + int col, int indent, boolean isField) { var prefix = " ".repeat(col); - var s = new StringBuilder(isField ? " " : prefix); + if (isField) { + s.append(" "); + } else { + s.append(prefix); + } if (jo.members().isEmpty()) { s.append("{}"); } else { s.append("{\n"); - jo.members().forEach((name, value) -> { - if (value instanceof JsonValue val) { - s.append(prefix) - .append(" ".repeat(indent)) - .append("\"") - .append(name) - .append("\":") - .append(Json.toDisplayString(val, col + indent, indent, true)) - .append(",\n"); - } else { - throw new InternalError("type mismatch"); - } + jo.members().forEach((name, val) -> { + s.append(prefix) + .append(" ".repeat(indent)) + .append("\"") + .append(name) + .append("\":"); + Json.toDisplayString(val, s, col + indent, indent, true); + s.append(",\n"); }); s.setLength(s.length() - 2); // trim final comma s.append("\n").append(prefix).append("}"); } - return s.toString(); } - private static String toDisplayString(JsonArray ja, int col, int indent, boolean isField) { + private static void toDisplayString(JsonArray ja, StringBuilder s, + int col, int indent, boolean isField) { var prefix = " ".repeat(col); - var s = new StringBuilder(isField ? " " : prefix); - if (ja.values().isEmpty()) { + if (isField) { + s.append(" "); + } else { + s.append(prefix); + } + if (ja.elements().isEmpty()) { s.append("[]"); } else { s.append("[\n"); - for (JsonValue v: ja.values()) { - if (v instanceof JsonValue jv) { - s.append(Json.toDisplayString(jv, col + indent, indent, false)).append(",\n"); - } else { - throw new InternalError("type mismatch"); - } + for (JsonValue v : ja.elements()) { + Json.toDisplayString(v, s, col + indent, indent, false); + s.append(",\n"); } s.setLength(s.length() - 2); // trim final comma/newline s.append("\n").append(prefix).append("]"); } - return s.toString(); } // no instantiation is allowed for this class diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java index 7ed3d71..e3ec4f2 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -32,44 +32,33 @@ import jdk.sandbox.internal.util.json.JsonArrayImpl; -/// The interface that represents JSON array. -/// -/// A `JsonArray` can be produced by {@link Json#parse(String)}. -/// Alternatively, {@link #of(List)} can be used to obtain a `JsonArray`. -/// -/// ## Example Usage -/// ```java -/// // Create from a List -/// JsonArray arr = JsonArray.of(List.of( -/// JsonString.of("first"), -/// JsonNumber.of(42), -/// JsonBoolean.of(true) -/// )); -/// -/// // Access elements -/// for (JsonValue value : arr.values()) { -/// switch (value) { -/// case JsonString s -> System.out.println("String: " + s.value()); -/// case JsonNumber n -> System.out.println("Number: " + n.toNumber()); -/// case JsonBoolean b -> System.out.println("Boolean: " + b.value()); -/// default -> System.out.println("Other: " + value); -/// } -/// } -/// ``` -/// -/// @since 99 +/** + * The interface that represents JSON array. + *

+ * A {@code JsonArray} can be produced by {@link Json#parse(String)}. + *

Alternatively, {@link #of(List)} can be used to obtain a {@code JsonArray}. + * + * @spec https://datatracker.ietf.org/doc/html/rfc8259#section-5 RFC 8259: + * The JavaScript Object Notation (JSON) Data Interchange Format - Arrays + * @since 99 + */ public non-sealed interface JsonArray extends JsonValue { - /// {@return an unmodifiable list of the `JsonValue` elements in - /// this `JsonArray`} - List values(); + /** + * {@return an unmodifiable list of the {@code JsonValue} elements in + * this {@code JsonArray}} + */ + @Override + List elements(); - /// {@return the `JsonArray` created from the given - /// list of `JsonValue`s} - /// - /// @param src the list of `JsonValue`s. Non-null. - /// @throws NullPointerException if `src` is `null`, or contains - /// any values that are `null` + /** + * {@return the {@code JsonArray} created from the given + * list of {@code JsonValue}s} + * + * @param src the list of {@code JsonValue}s. Non-null. + * @throws NullPointerException if {@code src} is {@code null}, or contains + * any values that are {@code null} + */ static JsonArray of(List src) { // Careful not to use List::contains on src for null checking which // throws NPE for immutable lists @@ -80,23 +69,27 @@ static JsonArray of(List src) { ); } - /// {@return `true` if the given object is also a `JsonArray` - /// and the two `JsonArray`s represent the same elements} Two - /// `JsonArray`s `ja1` and `ja2` represent the same - /// elements if `ja1.values().equals(ja2.values())`. - /// - /// @see #values() + /** + * {@return {@code true} if the given object is also a {@code JsonArray} + * and the two {@code JsonArray}s represent the same elements} Two + * {@code JsonArray}s {@code ja1} and {@code ja2} represent the same + * elements if {@code ja1.elements().equals(ja2.elements())}. + * + * @see #elements() + */ @Override boolean equals(Object obj); - /// {@return the hash code value for this `JsonArray`} The hash code value - /// of a `JsonArray` is derived from the hash code of `JsonArray`'s - /// {@link #values()}. - /// Thus, for two `JsonArray`s `ja1` and `ja2`, - /// `ja1.equals(ja2)` implies that `ja1.hashCode() == ja2.hashCode()` - /// as required by the general contract of {@link Object#hashCode}. - /// - /// @see #values() + /** + * {@return the hash code value for this {@code JsonArray}} The hash code value + * of a {@code JsonArray} is derived from the hash code of {@code JsonArray}'s + * {@link #elements()}. + * Thus, for two {@code JsonArray}s {@code ja1} and {@code ja2}, + * {@code ja1.equals(ja2)} implies that {@code ja1.hashCode() == ja2.hashCode()} + * as required by the general contract of {@link Object#hashCode}. + * + * @see #elements() + */ @Override int hashCode(); } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java index 98515be..fb3e648 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -27,43 +27,56 @@ import jdk.sandbox.internal.util.json.JsonBooleanImpl; -/// The interface that represents JSON boolean. -/// -/// A {@code JsonBoolean} can be produced by {@link Json#parse(String)}. -/// Alternatively, {@link #of(boolean)} can be used to -/// obtain a {@code JsonBoolean}. -/// -/// @since 99 +/** + * The interface that represents JSON boolean. + *

+ * A {@code JsonBoolean} can be produced by {@link Json#parse(String)}. + *

Alternatively, {@link #of(boolean)} can be used to + * obtain a {@code JsonBoolean}. + * + * @spec https://datatracker.ietf.org/doc/html/rfc8259#section-3 RFC 8259: + * The JavaScript Object Notation (JSON) Data Interchange Format - Values + * @since 99 + */ public non-sealed interface JsonBoolean extends JsonValue { - /// {@return the {@code boolean} value represented by this - /// {@code JsonBoolean}} - boolean value(); + /** + * {@return the {@code boolean} value represented by this + * {@code JsonBoolean}} + */ + @Override + boolean bool(); - /// {@return the {@code JsonBoolean} created from the given - /// {@code boolean}} - /// - /// @param src the given {@code boolean}. + /** + * {@return the {@code JsonBoolean} created from the given + * {@code boolean}} + * + * @param src the given {@code boolean}. + */ static JsonBoolean of(boolean src) { return src ? JsonBooleanImpl.TRUE : JsonBooleanImpl.FALSE; } - /// {@return {@code true} if the given object is also a {@code JsonBoolean} - /// and the two {@code JsonBoolean}s represent the same boolean value} Two - /// {@code JsonBoolean}s {@code jb1} and {@code jb2} represent the same - /// boolean values if {@code jb1.value().equals(jb2.value())}. - /// - /// @see #value() + /** + * {@return {@code true} if the given object is also a {@code JsonBoolean} + * and the two {@code JsonBoolean}s represent the same boolean value} Two + * {@code JsonBoolean}s {@code jb1} and {@code jb2} represent the same + * boolean values if {@code jb1.bool().equals(jb2.bool())}. + * + * @see #bool() + */ @Override boolean equals(Object obj); - /// {@return the hash code value for this {@code JsonBoolean}} The hash code value - /// of a {@code JsonBoolean} is derived from the hash code of {@code JsonBoolean}'s - /// {@link #value()}. Thus, for two {@code JsonBooleans}s {@code jb1} and {@code jb2}, - /// {@code jb1.equals(jb2)} implies that {@code jb1.hashCode() == jb2.hashCode()} - /// as required by the general contract of {@link Object#hashCode}. - /// - /// @see #value() + /** + * {@return the hash code value for this {@code JsonBoolean}} The hash code value + * of a {@code JsonBoolean} is derived from the hash code of {@code JsonBoolean}'s + * {@link #bool()}. Thus, for two {@code JsonBooleans}s {@code jb1} and {@code jb2}, + * {@code jb1.equals(jb2)} implies that {@code jb1.hashCode() == jb2.hashCode()} + * as required by the general contract of {@link Object#hashCode}. + * + * @see #bool() + */ @Override int hashCode(); } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java index 90e2411..92f3949 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java @@ -27,24 +27,32 @@ import jdk.sandbox.internal.util.json.JsonNullImpl; -/// The interface that represents JSON null. -/// -/// A {@code JsonNull} can be produced by {@link Json#parse(String)}. -/// Alternatively, {@link #of()} can be used to obtain a {@code JsonNull}. -/// -/// @since 99 +/** + * The interface that represents JSON null. + *

+ * A {@code JsonNull} can be produced by {@link Json#parse(String)}. + *

Alternatively, {@link #of()} can be used to obtain a {@code JsonNull}. + * + * @since 99 + */ public non-sealed interface JsonNull extends JsonValue { - /// {@return a {@code JsonNull}} + /** + * {@return a {@code JsonNull}} + */ static JsonNull of() { return JsonNullImpl.NULL; } - /// {@return true if the given {@code obj} is a {@code JsonNull}} + /** + * {@return true if the given {@code obj} is a {@code JsonNull}} + */ @Override boolean equals(Object obj); - /// {@return the hash code value of this {@code JsonNull}} + /** + * {@return the hash code value of this {@code JsonNull}} + */ @Override int hashCode(); } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java index 9e5fe8d..f49c8de 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -25,159 +25,169 @@ package jdk.sandbox.java.util.json; -import java.math.BigDecimal; -import java.math.BigInteger; - import jdk.sandbox.internal.util.json.JsonNumberImpl; -/// The interface that represents JSON number, an arbitrary-precision -/// number represented in base 10 using decimal digits. -/// -/// A {@code JsonNumber} can be produced by {@link Json#parse(String)}. -/// Alternatively, {@link #of(double)} and its overloads can be used to obtain -/// a {@code JsonNumber} from a {@code Number}. -/// When a JSON number is parsed, a {@code JsonNumber} object is created -/// as long as the parsed value adheres to the JSON number -/// [syntax](https://datatracker.ietf.org/doc/html/rfc8259#section-6). -/// The value of the {@code JsonNumber} -/// can be retrieved from {@link #toString()} as the string representation -/// from which the JSON number is originally parsed, with -/// {@link #toNumber()} as a {@code Number} instance, or with -/// {@link #toBigDecimal()}. -/// -/// @spec https://datatracker.ietf.org/doc/html/rfc8259#section-6 RFC 8259: -/// The JavaScript Object Notation (JSON) Data Interchange Format - Numbers -/// @since 99 +/** + * The interface that represents JSON number, an arbitrary-precision + * number represented in base 10 using decimal digits. + *

+ * A {@code JsonNumber} can be produced by {@link Json#parse(String)}. + * When a JSON number is parsed, a {@code JsonNumber} object is created + * as long as the parsed value adheres to the JSON number + * + * syntax. + * Alternatively, {@link #of(double)}, {@link #of(long)}, or {@link #of(String)} + * can be used to obtain a {@code JsonNumber}. + * The value of the {@code JsonNumber} can be retrieved as a {@code long} + * with {@link #toLong()} or as a {@code double} with {@link #toDouble()}. + * {@link #toString()} can be used to return the string representation of + * the JSON number. + * + * @apiNote + * To avoid precision loss when converting JSON numbers to Java types, or when + * converting JSON numbers outside the range of {@code long} or {@code double}, + * use {@link #toString()} to create arbitrary-precision Java objects, for + * example, + * {@snippet lang="java" : + * new BigDecimal(JsonNumber.toString()) + * // or if an integral number is preferred + * new BigInteger(JsonNumber.toString()) + * // for cases with an exponent or zero fractional part + * new BigDecimal(JsonNumber.toString()).toBigIntegerExact() + * } + * + * @spec https://datatracker.ietf.org/doc/html/rfc8259#section-6 RFC 8259: + * The JavaScript Object Notation (JSON) Data Interchange Format - Numbers + * @since 99 + */ public non-sealed interface JsonNumber extends JsonValue { - /// {@return the {@code Number} parsed or translated from the - /// {@link #toString string representation} of this {@code JsonNumber}} - /// - /// This method operates on the string representation and depending on that - /// representation computes and returns an instance of {@code Long}, {@code BigInteger}, - /// {@code Double}, or {@code BigDecimal}. - /// - /// If the string representation is the decimal string representation of - /// a {@code long} value, parsable by {@link Long#parseLong(String)}, - /// then that {@code long} value is returned in its boxed form as {@code Long}. - /// Otherwise, if the string representation is the decimal string representation of a - /// {@code BigInteger}, translatable by {@link BigInteger#BigInteger(String)}, - /// then that {@code BigInteger} is returned. - /// Otherwise, if the string representation is the decimal string representation of - /// a {@code double} value, parsable by {@link Double#parseDouble(String)}, - /// and the {@code double} value is not {@link Double#isInfinite() infinite}, then that - /// {@code double} value is returned in its boxed form as {@code Double}. - /// Otherwise, and in all other cases, the string representation is the decimal string - /// representation of a {@code BigDecimal}, translatable by - /// {@link BigDecimal#BigDecimal(String)}, and that {@code BigDecimal} is - /// returned. - /// - /// The computation may not preserve all information in the string representation. - /// In all of the above cases one or more leading zero digits are not preserved. - /// In the third case, returning {@code Double}, decimal to binary conversion may lose - /// decimal precision, and will not preserve one or more trailing zero digits in the fraction - /// part. - /// - /// @apiNote - /// Pattern matching can be used to match against {@code Long}, - /// {@code Double}, {@code BigInteger}, or {@code BigDecimal} reference - /// types. For example: - /// {@snippet lang=java: - /// switch(jsonNumber.toNumber()) { - /// case Long l -> { ... } - /// case Double d -> { ... } - /// case BigInteger bi -> { ... } - /// case BigDecimal bd -> { ... } - /// default -> { } // should not happen - /// } - ///} - /// @throws NumberFormatException if the {@code Number} cannot be parsed or translated from the string representation - /// @see #toBigDecimal() - /// @see #toString() - Number toNumber(); + /** + * {@return a {@code long} if it can be translated from the string + * representation of this {@code JsonNumber}} That is, it can be expressed + * as a whole number and is within the range of {@link Long#MIN_VALUE} and + * {@link Long#MAX_VALUE}. This occurs, even if the string contains an + * exponent or a fractional part consisting of only zero digits. For example, + * both the JSON number "123.0" and "1.23e2" produce a {@code long} value of + * "123". A {@code JsonAssertionException} is thrown when the numeric value + * cannot be represented as a {@code long}; for example, the value "5.5". + * + * @throws JsonAssertionException if this {@code JsonNumber} cannot + * be represented as a {@code long}. + */ + @Override + long toLong(); - /// {@return the {@code BigDecimal} translated from the - /// {@link #toString string representation} of this {@code JsonNumber}} - /// - /// The string representation is the decimal string representation of a - /// {@code BigDecimal}, translatable by {@link BigDecimal#BigDecimal(String)}, - /// and that {@code BigDecimal} is returned. - /// - /// The translation may not preserve all information in the string representation. - /// The sign is not preserved for the decimal string representation {@code -0.0}. One or more - /// leading zero digits are not preserved. - /// - /// @throws NumberFormatException if the {@code BigDecimal} cannot be translated from the string representation - BigDecimal toBigDecimal(); + /** + * {@return a finite {@code double} if it can be translated from the string + * representation of this {@code JsonNumber}} If the string representation + * is outside the range of {@link Double#MAX_VALUE -Double.MAX_VALUE} and + * {@link Double#MAX_VALUE}, a {@code JsonAssertionException} is thrown. + * + * @apiNote Callers of this method should be aware of the potential loss in + * precision when the string representation of the JSON number is translated + * to a {@code double}. + * @implNote The JDK reference implementation uses {@link + * Double#parseDouble(String)} to perform the conversion from string to + * finite double. + * + * @throws JsonAssertionException if this {@code JsonNumber} cannot + * be represented as a finite {@code double}. + */ + @Override + double toDouble(); - /// Creates a JSON number whose string representation is the - /// decimal string representation of the given {@code double} value, - /// produced by applying the value to {@link Double#toString(double)}. - /// - /// @param num the given {@code double} value. - /// @return a JSON number created from a {@code double} value - /// @throws IllegalArgumentException if the given {@code double} value - /// is {@link Double#isNaN() NaN} or is {@link Double#isInfinite() infinite}. + /** + * Creates a JSON number from the given {@code double} value. + * The string representation of the JSON number created is produced by applying + * {@link Double#toString(double)} on {@code num}. + * + * @param num the given {@code double} value. + * @return a JSON number created from the {@code double} value + * @throws IllegalArgumentException if the given {@code double} value + * is not a finite floating-point value ({@link Double#NaN NaN}, + * {@link Double#POSITIVE_INFINITY positive infinity}, or + * {@link Double#NEGATIVE_INFINITY negative infinity}). + */ static JsonNumber of(double num) { - // non-integral types - return new JsonNumberImpl(num); + if (!Double.isFinite(num)) { + throw new IllegalArgumentException("Not a valid JSON number"); + } + var str = Double.toString(num); + return new JsonNumberImpl(str.toCharArray(), 0, str.length(), 0, 0); } - /// Creates a JSON number whose string representation is the - /// decimal string representation of the given {@code long} value, - /// produced by applying the value to {@link Long#toString(long)}. - /// - /// @param num the given {@code long} value. - /// @return a JSON number created from a {@code long} value + /** + * Creates a JSON number from the given {@code long} value. + * The string representation of the JSON number created is produced by applying + * {@link Long#toString(long)} on {@code num}. + * + * @param num the given {@code long} value. + * @return a JSON number created from the {@code long} value + */ static JsonNumber of(long num) { - // integral types - return new JsonNumberImpl(num); - } - - /// Creates a JSON number whose string representation is the - /// string representation of the given {@code BigInteger} value. - /// - /// @param num the given {@code BigInteger} value. - /// @return a JSON number created from a {@code BigInteger} value - static JsonNumber of(BigInteger num) { - return new JsonNumberImpl(num); + var str = Long.toString(num); + return new JsonNumberImpl(str.toCharArray(), 0, str.length(), -1, -1); } - /// Creates a JSON number whose string representation is the - /// string representation of the given {@code BigDecimal} value. - /// - /// @param num the given {@code BigDecimal} value. - /// @return a JSON number created from a {@code BigDecimal} value - static JsonNumber of(BigDecimal num) { - return new JsonNumberImpl(num); + /** + * Creates a JSON number from the given {@code String} value. + * The string representation of the JSON number created is equivalent to + * {@code num}. + * + * @implNote The value returned is equivalent to calling: + * {@snippet lang = "java": + * if (Json.parse(num) instanceof JsonNumber jn) { + * return jn; + * } + * } + * + * @param num the given {@code String} value. + * @throws IllegalArgumentException if {@code num} is not a valid string + * representation of a JSON number. + * @return a JSON number created from the {@code String} value + */ + static JsonNumber of(String num) { + try { + if (Json.parse(num) instanceof JsonNumber jn) { + return jn; + } + } catch (JsonParseException ignored) {} + throw new IllegalArgumentException("Not a JSON number"); } - /// {@return the decimal string representation of this {@code JsonNumber}} - /// - /// If this {@code JsonNumber} is created by parsing a JSON number in a JSON document, - /// it preserves the string representation in the document, regardless of its - /// precision or range. For example, a JSON number like - /// {@code 3.141592653589793238462643383279} in the JSON document will be - /// returned exactly as it appears. - /// If this {@code JsonNumber} is created via one of the factory methods, - /// such as {@link JsonNumber#of(double)}, then the string representation is - /// specified by the factory method. + /** + * {@return the string representation of this {@code JsonNumber}} + * + * If this {@code JsonNumber} is created by parsing a JSON number in a JSON document, + * it preserves the string representation in the document, regardless of its + * precision or range. For example, a JSON number like + * {@code 3.141592653589793238462643383279} in the JSON document will be + * returned exactly as it appears. + * If this {@code JsonNumber} is created via one of the factory methods, + * such as {@link JsonNumber#of(double)}, then the string representation is + * specified by the factory method. + */ @Override String toString(); - /// {@return true if the given {@code obj} is equal to this {@code JsonNumber}} - /// The comparison is based on the string representation of this {@code JsonNumber}, - /// ignoring the case. - /// - /// @see #toString() + /** + * {@return true if the given {@code obj} is equal to this {@code JsonNumber}} + * The comparison is based on the string representation of this {@code JsonNumber}, + * ignoring the case. + * + * @see #toString() + */ @Override boolean equals(Object obj); - /// {@return the hash code value of this {@code JsonNumber}} The returned hash code - /// is derived from the string representation of this {@code JsonNumber}, - /// ignoring the case. - /// - /// @see #toString() + /** + * {@return the hash code value of this {@code JsonNumber}} The returned hash code + * is derived from the string representation of this {@code JsonNumber}, + * ignoring the case. + * + * @see #toString() + */ @Override int hashCode(); } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java index b27385f..7b4f8e7 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -32,44 +32,39 @@ import jdk.sandbox.internal.util.json.JsonObjectImpl; -/// The interface that represents JSON object. -/// -/// A `JsonObject` can be produced by a {@link Json#parse(String)}. -/// Alternatively, {@link #of(Map)} can be used to obtain a `JsonObject`. -/// Implementations of `JsonObject` cannot be created from sources that -/// contain duplicate member names. If duplicate names appear during -/// a {@link Json#parse(String)}, a `JsonParseException` is thrown. -/// -/// ## Example Usage -/// ```java -/// // Create from a Map -/// JsonObject obj = JsonObject.of(Map.of( -/// "name", JsonString.of("Alice"), -/// "age", JsonNumber.of(30), -/// "active", JsonBoolean.of(true) -/// )); -/// -/// // Access members -/// JsonString name = (JsonString) obj.members().get("name"); -/// System.out.println(name.value()); // "Alice" -/// ``` -/// -/// @since 99 +/** + * 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. + * + * @spec https://datatracker.ietf.org/doc/html/rfc8259#section-4 RFC 8259: + * The JavaScript Object Notation (JSON) Data Interchange Format - Objects + * @since 99 + */ public non-sealed interface JsonObject extends JsonValue { - /// {@return an unmodifiable map of the `String` to `JsonValue` - /// members in this `JsonObject`} + /** + * {@return an unmodifiable map of the {@code String} to {@code JsonValue} + * members in this {@code JsonObject}} + */ + @Override Map members(); - /// {@return the `JsonObject` created from the given - /// map of `String` to `JsonValue`s} - /// - /// The `JsonObject`'s members occur in the same order as the given - /// map's entries. - /// - /// @param map the map of `JsonValue`s. Non-null. - /// @throws NullPointerException if `map` is `null`, contains - /// any keys that are `null`, or contains any values that are `null`. + /** + * {@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() @@ -78,22 +73,26 @@ static JsonObject of(Map map) { (ignored, v) -> v, LinkedHashMap::new))); } - /// {@return `true` if the given object is also a `JsonObject` - /// and the two `JsonObject`s represent the same mappings} Two - /// `JsonObject`s `jo1` and `jo2` represent the same - /// mappings if `jo1.members().equals(jo2.members())`. - /// - /// @see #members() + /** + * {@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 `JsonObject`} The hash code value - /// of a `JsonObject` is derived from the hash code of `JsonObject`'s - /// {@link #members()}. Thus, for two `JsonObject`s `jo1` and `jo2`, - /// `jo1.equals(jo2)` implies that `jo1.hashCode() == jo2.hashCode()` - /// as required by the general contract of {@link Object#hashCode}. - /// - /// @see #members() + /** + * {@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/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java index 88c64a5..43903e0 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -25,43 +25,57 @@ package jdk.sandbox.java.util.json; -import java.io.Serial; +import java.io.Serial; -/// Signals that an error has been detected while parsing the -/// JSON document. -/// -/// @since 99 +/** + * Signals that an error has been detected while parsing the + * JSON document. This exception is thrown if the value supplied + * to the {@link Json#parse(String) Json::parse} methods is not valid JSON + * syntax, or contains a JSON object with duplicate names. + * + * @since 99 + */ public class JsonParseException extends RuntimeException { @Serial private static final long serialVersionUID = 7022545379651073390L; - /// Position of the error row in the document - /// @serial - private final int row; + /** + * Position of the error line in the document + * @serial + */ + private final int line; - /// Position of the error column in the document - /// @serial - private final int col; + /** + * Position of the error position in the document + * @serial + */ + private final int pos; - /// Constructs a JsonParseException with the specified detail message. - /// @param message the detail message - /// @param row the row of the error on parsing the document - /// @param col the column of the error on parsing the document - public JsonParseException(String message, int row, int col) { + /** + * Constructs a JsonParseException with the specified detail message. + * @param message the detail message + * @param line the line of the error on parsing the document + * @param pos the position of the error on parsing the document + */ + public JsonParseException(String message, int line, int pos) { super(message); - this.row = row; - this.col = col; + this.line = line; + this.pos = pos; } - /// {@return the row of the error on parsing the document} - public int getErrorRow() { - return row; + /** + * {@return the line of the error on parsing the document} + */ + public int getErrorLine() { + return line; } - /// {@return the column of the error on parsing the document} - public int getErrorColumn() { - return col; + /** + * {@return the position of the error on parsing the document} + */ + public int getErrorPosition() { + return pos; } } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java index 234efdc..a1f2bce 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -28,68 +28,85 @@ import java.util.Objects; import jdk.sandbox.internal.util.json.JsonStringImpl; +import jdk.sandbox.internal.util.json.Utils; -/// The interface that represents a JSON string. -/// -/// A {@code JsonString} can be produced by a {@link Json#parse(String)}. -/// Within a valid JSON String, any character may be escaped using either a -/// two-character escape sequence (if applicable) or a Unicode escape sequence. -/// Quotation mark (U+0022), reverse solidus (U+005C), and the control characters -/// (U+0000 through U+001F) must be escaped. -/// -/// Alternatively, {@link #of(String)} can be used to obtain a {@code JsonString} -/// directly from a {@code String}. The following expressions are all equivalent, -/// {@snippet lang="java" : -/// Json.parse("\"foo\\t\""); -/// Json.parse("\"foo\\u0009\""); -/// JsonString.of("foo\t"); -/// } -/// -/// @spec https://datatracker.ietf.org/doc/html/rfc8259#section-7 RFC 8259: -/// The JavaScript Object Notation (JSON) Data Interchange Format - Strings -/// @since 99 +/** + * The interface that represents a JSON string. + *

+ * A {@code JsonString} can be produced by a {@link Json#parse(String)}. + * Within a valid JSON string, any character may be escaped using either a + * two-character escape sequence (if applicable) or a Unicode escape sequence. + * Quotation mark (U+0022), reverse solidus (U+005C), and the control characters + * (U+0000 through U+001F) must be escaped. + *

Alternatively, {@link #of(String)} can be used to obtain a {@code JsonString} + * directly from a {@code String}. The {@code JsonString} instances produced by + * the following expressions are all equivalent, + * {@snippet lang = "java": + * Json.parse("\"foo\\t\""); + * Json.parse("\"foo\\u0009\""); + * JsonString.of("foo\t"); + *} + * + * @spec https://datatracker.ietf.org/doc/html/rfc8259#section-7 RFC 8259: + * The JavaScript Object Notation (JSON) Data Interchange Format - Strings + * @since 99 + */ public non-sealed interface JsonString extends JsonValue { - /// {@return the {@code JsonString} created from the given - /// {@code String}} - /// - /// @param value the given {@code String} used as the {@code value} of this - /// {@code JsonString}. Non-null. - /// @throws NullPointerException if {@code value} is {@code null} - static JsonString of(String value) { - Objects.requireNonNull(value); - return new JsonStringImpl(value); + /** + * {@return the {@code JsonString} created from the given + * {@code String}} + * + * @param src the given source {@code String}. Non-null. + * @throws NullPointerException if {@code src} is {@code null} + */ + static JsonString of(String src) { + var escaped = '"' + Utils.escape(Objects.requireNonNull(src)) + '"'; + return new JsonStringImpl(escaped.toCharArray(), 0, escaped.length(), + escaped.length() != src.length() + 2); } - /// {@return the JSON {@code String} represented by this {@code JsonString}} - /// If this {@code JsonString} was created by parsing a JSON document, it - /// preserves the text representation of the corresponding JSON String. Otherwise, - /// the {@code value} is escaped to produce the JSON {@code String}. - /// - /// @see #value() + /** + * {@return the JSON string represented by this {@code JsonString}} + * If this {@code JsonString} was created by parsing a JSON document, it + * preserves the original text representation of the corresponding JSON + * string. Otherwise, the source {@code String} passed to the factory method + * {@link #of(String)} is used to generate the JSON string, with special + * characters properly escaped. + * + * @see #string() + */ + @Override String toString(); - /// {@return the {@code String} value represented by this {@code JsonString}} - /// If this {@code JsonString} was created by parsing a JSON document, any - /// escaped characters in the original JSON document are converted to their - /// unescaped form. - /// - /// @see #toString() - String value(); + /** + * {@return the {@code String} value represented by this {@code JsonString}} + * If this {@code JsonString} was created by parsing a JSON document, any + * escaped characters in the original JSON document are converted to their + * unescaped form. + * + * @see #toString() + */ + @Override + String string(); - /// {@return true if the given {@code obj} is equal to this {@code JsonString}} - /// Two {@code JsonString}s {@code js1} and {@code js2} represent the same value - /// if {@code js1.value().equals(js2.value())}. - /// - /// @see #value() + /** + * {@return true if the given {@code obj} is equal to this {@code JsonString}} + * Two {@code JsonString}s {@code js1} and {@code js2} represent the same value + * if {@code js1.string().equals(js2.string())}. + * + * @see #string() + */ @Override boolean equals(Object obj); - /// {@return the hash code value of this {@code JsonString}} The hash code of a - /// {@code JsonString} is derived from the hash code of {@code JsonString}'s - /// {@link #value()}. - /// - /// @see #value() + /** + * {@return the hash code value of this {@code JsonString}} The hash code of a + * {@code JsonString} is derived from the hash code of {@code JsonString}'s + * {@link #string()}. + * + * @see #string() + */ @Override int hashCode(); } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java index b7659f4..b539d86 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -25,73 +25,285 @@ package jdk.sandbox.java.util.json; +import jdk.sandbox.internal.util.json.Utils; -/// The interface that represents a JSON value. -/// -/// Instances of `JsonValue` are immutable and thread safe. -/// -/// A `JsonValue` can be produced by {@link Json#parse(String)} or {@link -/// Json#fromUntyped(Object)}. See {@link #toString()} for converting a `JsonValue` -/// to its corresponding JSON String. -/// -/// ## Example -/// ```java -/// List values = Arrays.asList("foo", true, 25); -/// JsonValue json = Json.fromUntyped(values); -/// json.toString(); // returns "[\"foo\",true,25]" -/// ``` -/// -/// ## Pattern Matching -/// ```java -/// JsonValue value = Json.parse(jsonString); -/// switch (value) { -/// case JsonObject obj -> processObject(obj); -/// case JsonArray arr -> processArray(arr); -/// case JsonString str -> processString(str); -/// case JsonNumber num -> processNumber(num); -/// case JsonBoolean bool -> processBoolean(bool); -/// case JsonNull n -> processNull(); -/// } -/// ``` -/// -/// A class implementing a non-sealed `JsonValue` sub-interface must adhere -/// to the following: -/// - The class's implementations of `equals`, `hashCode`, -/// and `toString` compute their results solely from the values -/// of the class's instance fields (and the members of the objects they -/// reference), not from the instance's identity. -/// - The class's methods treat instances as *freely substitutable* -/// when equal, meaning that interchanging any two instances `x` and -/// `y` that are equal according to `equals()` produces no -/// visible change in the behavior of the class's methods. -/// - The class performs no synchronization using an instance's monitor. -/// - The class does not provide any instance creation mechanism that promises -/// a unique identity on each method call—in particular, any factory -/// method's contract must allow for the possibility that if two independently-produced -/// instances are equal according to `equals()`, they may also be -/// equal according to `==`. -/// -/// Users of `JsonValue` instances should ensure the following: -/// - When two instances of `JsonValue` are equal (according to `equals()`), users -/// should not attempt to distinguish between their identities, whether directly via reference -/// equality or indirectly via an appeal to synchronization, identity hashing, -/// serialization, or any other identity-sensitive mechanism. -/// - Synchronization on instances of `JsonValue` is strongly discouraged, -/// because the programmer cannot guarantee exclusive ownership of the -/// associated monitor. -/// -/// @since 99 -public sealed interface JsonValue - permits JsonString, JsonNumber, JsonObject, JsonArray, JsonBoolean, JsonNull { +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; - /// {@return the String representation of this `JsonValue` that conforms - /// to the JSON syntax} If this `JsonValue` is created by parsing a - /// JSON document, it preserves the text representation of the corresponding - /// JSON element, except that the returned string does not contain any white - /// spaces or newlines to produce a compact representation. - /// For a String representation suitable for display, use - /// {@link Json#toDisplayString(JsonValue, int)}. - /// - /// @see Json#toDisplayString(JsonValue, int) +/** + * The interface that represents a JSON value. A {@code JsonValue} can be + * produced by parsing a JSON document with {@link Json#parse(String)}. Extracting + * a value is done in a 2-step process using {@link ##access access} and {@link + * ##conversion conversion} methods. The {@link ##generation generation} method + * produces the JSON compliant text from the {@code JsonValue}. + *

Navigating JSON documents

+ * Use the access methods to navigate to the desired JSON element. {@link + * #get(String)} is provided for JSON object and {@link #element(int)} for JSON array. + * Given the JSON document: + * {@snippet lang=java: + * JsonValue json = Json.parse(""" + * { "foo": ["bar", true, 42], "baz": null } + * """); + * } + * the JSON String "bar" can be accessed as follows: + * {@snippet lang=java: + * JsonValue foo0 = json.get("foo").element(0); + * } + * If an access method is invoked on an incompatible JSON type (for example, + * calling {@code get(String)} on a JSON array), a {@code JsonAssertionException} + * is thrown. + *

+ * Once the desired JSON element is reached, call the corresponding conversion + * method to retrieve an appropriate Java value from the {@code JsonValue}. + *

Converting JSON values to Java values

+ * Use the conversion methods to produce a Java value from the {@code + * JsonValue}. Each conversion methods corresponds to a JSON type: + *
    + *
  • {@code string()} returns a String that represents the JSON string + * with all RFC 8259 JSON escapes translated to their corresponding + * characters.
  • + *
  • {@code toLong()} returns a long provided the JSON number is a whole + * number within range of {@code Long.MIN_VALUE} and {@code Long.MAX_VALUE}. + *
  • + *
  • {@code toDouble()} returns a double provided the JSON number is + * within range of {@code -Double.MAX_VALUE} and {@code Double.MAX_VALUE}. + *
  • + *
  • {@code bool()} returns {@code true} or {@code false} for JSON + * boolean literals.
  • + *
  • {@code members()} returns an unmodifiable map of {@code String} to + * {@code JsonValue} for JSON object, guaranteed to contain neither null + * keys nor null values. If the JSON object contains no members, an empty + * map is returned. + *
  • + *
  • {@code elements()} returns an unmodifiable list of {@code JsonValue} + * for JSON array, guaranteed to contain non-null values. If the JSON array + * contains no elements, an empty list is returned.
  • + *
+ * For example, + * {@snippet lang=java: + * String bar = foo0.string(); + * } + * The code above retrieves the Java String "bar" from the JSON element {@code foo0}. + * If an incorrect conversion method is used, which does not correspond to the matching + * JSON type, for example {@code foo0.bool()}, a {@code JsonAssertionException} is thrown. + *

+ * These conversion methods always return a value when the {@code JsonValue} is + * of the correct JSON type. The exceptions are {@code toLong()} and + * {@code toDouble()}; the {@code to} prefix implies that they may throw a + * {@code JsonAssertionException} even when the {@code JsonValue} is a JSON + * number, for example if it is outside their supported ranges. + *

Subtypes of JsonValue

+ * The {@code JsonValue} subtypes correspond to the JSON types. For example, + * {@code JsonString} to JSON string. If the type of JSON value is unknown, it can + * be retrieved as follows: + * {@snippet lang=java: + * switch (json.get("foo")) { + * case JsonString js -> js.string(); // handle the value as JSON string + * case JsonArray ja -> ja.element(0).string(); // handle the value as JSON array + * default -> throw new JsonAssertionException("unexpected type"); + * } + * } + *

Missing Object Members

+ * There are times when the member in a JSON object is optional. For those + * cases, use the access method {@link #getOrAbsent(String)} which returns an + * Optional of JsonValue. For example: + * {@snippet lang=java: + * json.getOrAbsent("foo") + * .ifPresent(IO::println) + * } + * This example only prints the value if the member named "foo" exists. + *

Handling of null

+ * In some JSON documents, JSON null is used to signify absence. + * For those cases, use the access method {@link #valueOrNull()} which returns an + * Optional of JsonValue. For example: + * {@snippet lang=java: + * json.get("baz") + * .valueOrNull() + * .ifPresent(IO::println) + * } + * This example only prints the value if the member named "baz" is not a JSON + * null. + *

Generating JSON documents

+ * {@code JsonValue} overrides {@link Object#toString()} to generate RFC 8259 compliant + * JSON text in a compact representation with white spaces eliminated. + * For generating JSON documents suitable for display, use + * the generation method {@link Json#toDisplayString(JsonValue, int)} instead. + *

+ * Instances of {@code JsonValue} are immutable and thread safe. + * + * @implSpec A class implementing a non-sealed {@code JsonValue} sub-interface + * must adhere to the + * value-based + * class requirements. + * + * @since 99 + */ +public sealed interface JsonValue permits JsonString, JsonNumber, JsonObject, JsonArray, JsonBoolean, JsonNull { + + /** + * {@return the String representation of this {@code JsonValue} that conforms + * to the JSON syntax} If this {@code JsonValue} is created by parsing a + * JSON document, it preserves the text representation of the corresponding + * JSON element, except that the returned string does not contain any white + * spaces or newlines to produce a compact representation. + * For a String representation suitable for display, use + * {@link Json#toDisplayString(JsonValue, int)}. + * + * @see Json#toDisplayString(JsonValue, int) + */ String toString(); + + /** + * {@return the {@code boolean} value represented by a {@code JsonBoolean}} + * + * @implSpec + * The default implementation throws {@code JsonAssertionException}. + * + * @throws JsonAssertionException if this {@code JsonValue} is not an instance of {@code JsonBoolean}. + */ + default boolean bool() { + throw Utils.composeTypeError(this, "JsonBoolean"); + } + + /** + * {@return this {@code JsonValue} as a {@code long}} + * + * @implSpec + * The default implementation throws {@code JsonAssertionException}. + * + * @throws JsonAssertionException if this {@code JsonValue} is not an instance + * of {@code JsonNumber} nor can be represented as a {@code long}. + */ + default long toLong() { + throw Utils.composeTypeError(this, "JsonNumber"); + } + + /** + * {@return this {@code JsonValue} as a {@code double}} + * + * @implSpec + * The default implementation throws {@code JsonAssertionException}. + * + * @throws JsonAssertionException if this {@code JsonValue} is not an instance + * of {@code JsonNumber} nor can be represented as a {@code double}. + */ + default double toDouble() { + throw Utils.composeTypeError(this, "JsonNumber"); + } + + /** + * {@return the {@code String} value represented by a {@code JsonString}} + * + * @implSpec + * The default implementation throws {@code JsonAssertionException}. + * + * @throws JsonAssertionException if this {@code JsonValue} is not an instance of {@code JsonString}. + */ + default String string() { + throw Utils.composeTypeError(this, "JsonString"); + } + + /** + * {@return an {@code Optional} containing this {@code JsonValue} if it + * is not an instance of {@code JsonNull}, otherwise an empty {@code Optional}} + * + * @implSpec + * The default implementation returns {@link Optional#empty} if this + * {@code JsonValue} is an instance of {@code JsonNull}; otherwise + * {@link Optional#of} given this {@code JsonValue}. + */ + default Optional valueOrNull() { + return switch (this) { + case JsonNull ignored -> Optional.empty(); + case JsonValue ignored -> Optional.of(this); + }; + } + + /** + * {@return the {@link JsonArray#elements() elements} of a {@code JsonArray}} + * + * @implSpec + * The default implementation throws {@code JsonAssertionException}. + * + * @throws JsonAssertionException if this {@code JsonValue} is not an instance of {@code JsonArray}. + */ + default List elements() { + throw Utils.composeTypeError(this, "JsonArray"); + } + + /** + * {@return the {@link JsonObject#members() members} of a {@code JsonObject}} + * + * @implSpec + * The default implementation throws {@code JsonAssertionException}. + * + * @throws JsonAssertionException if this {@code JsonValue} is not an instance of {@code JsonObject}. + */ + default Map members() { + throw Utils.composeTypeError(this, "JsonObject"); + } + + /** + * {@return the {@code JsonValue} associated with the given member name of a {@code JsonObject}} + * + * @implSpec + * The default implementation obtains a {@code JsonValue} which is the result + * of invoking {@link #members()}{@code .get(name)}. If {@code name} is absent, + * {@code JsonAssertionException} is thrown. + * + * @param name the member name + * @throws NullPointerException if the member name is {@code null} + * @throws JsonAssertionException if this {@code JsonValue} is not an instance of a {@code JsonObject} or + * there is no association with the member name + */ + default JsonValue get(String name) { + Objects.requireNonNull(name); + return switch (members().get(name)) { + case JsonValue jv -> jv; + case null -> throw Utils.composeError(this, + "JsonObject member \"%s\" does not exist.".formatted(name)); + }; + } + + /** + * {@return an {@code Optional} containing the {@code JsonValue} associated with the given member + * name of a {@code JsonObject}, otherwise if there is no association an empty {@code Optional}} + * + * @implSpec + * The default implementation obtains an {@code Optional} by invoking {@link + * #members()}{@code .get(name)}, which is then passed to {@link Optional#ofNullable}. + * + * @param name the member name + * @throws NullPointerException if the member name is {@code null} + * @throws JsonAssertionException if this {@code JsonValue} is not an instance of a {@code JsonObject} + */ + default Optional getOrAbsent(String name) { + Objects.requireNonNull(name); + return Optional.ofNullable(members().get(name)); + } + + /** + * {@return the {@code JsonValue} associated with the given index of a {@code JsonArray}} + * + * @implSpec + * The default implementation obtains a {@code JsonValue} which is the result + * of invoking {@link #elements()}{@code .get(index)}. If {@code index} is + * out of bounds, {@code JsonAssertionException} is thrown. + * + * @param index the index of the array + * @throws JsonAssertionException if this {@code JsonValue} is not an instance of a {@code JsonArray} + * or the given index is outside the bounds + */ + default JsonValue element(int index) { + List elements = elements(); + try { + return elements.get(index); + } catch (IndexOutOfBoundsException ignored) { + throw Utils.composeError(this, + "JsonArray index %d out of bounds for length %d." + .formatted(index, elements.size())); + } + } } diff --git a/json-java21/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 index 0cb20b5..ff50ac7 100644 --- a/json-java21/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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -23,168 +23,47 @@ * questions. */ -/// Provides APIs for parsing JSON text, creating `JsonValue`s, and -/// offering a mapping between a `JsonValue` and its corresponding Java Object. -/// -/// ## Design -/// This API is designed so that JSON values are composed as Algebraic -/// Data Types (ADTs) defined by interfaces. Each JSON value is represented as a -/// sealed `JsonValue` _sum_ type, which can be -/// pattern-matched into one of the following _product_ types: `JsonObject`, -/// `JsonArray`, `JsonString`, `JsonNumber`, `JsonBoolean`, -/// `JsonNull`. These product types are defined as non-sealed interfaces that -/// allow flexibility in the implementation of the type. For example, `JsonArray` -/// is defined as follows: -/// ```java -/// public non-sealed interface JsonArray extends JsonValue -/// ``` -/// -/// This API relies on pattern matching to allow for the extraction of a -/// JSON Value in a _single and class safe expression_ as follows: -/// ```java -/// JsonValue doc = Json.parse(text); -/// if (doc instanceof JsonObject o && o.members() instanceof Map members -/// && members.get("name") instanceof JsonString js && js.value() instanceof String name -/// && members.get("age") instanceof JsonNumber jn && jn.toNumber() instanceof long age) { -/// // can use both "name" and "age" from a single expression -/// } -/// ``` -/// -/// Both `JsonValue` instances and their underlying values are immutable. -/// -/// ## Parsing -/// -/// Parsing produces a `JsonValue` from JSON text and is done using either -/// {@link Json#parse(java.lang.String)} or {@link Json#parse(char[])}. A successful -/// parse indicates that the JSON text adheres to the -/// [JSON grammar](https://datatracker.ietf.org/doc/html/rfc8259). -/// The parsing APIs provided do not accept JSON text that contain JSON Objects -/// with duplicate names. -/// -/// For the reference JDK implementation, `JsonValue`s created via parsing -/// procure their underlying values _lazily_. -/// -/// ## Formatting -/// -/// Formatting of a `JsonValue` is performed with either {@link -/// JsonValue#toString()} or {@link Json#toDisplayString(JsonValue, int)}. -/// These methods produce formatted String representations of a `JsonValue`. -/// The returned text adheres to the JSON grammar defined in RFC 8259. -/// `JsonValue.toString()` produces the most compact representation which does not -/// include extra whitespaces or line-breaks, preferable for network transaction -/// or storage. `Json.toDisplayString(JsonValue, int)` produces a text representation that -/// is human friendly, preferable for debugging or logging. -/// -/// --- -/// -/// ## Usage Notes from Unofficial Backport -/// -/// ### Major Classes -/// -/// - {@link Json} - Main entry point for parsing and converting JSON -/// - {@link JsonValue} - Base sealed interface for all JSON values -/// - {@link JsonObject} - Represents JSON objects (key-value pairs) -/// - {@link JsonArray} - Represents JSON arrays -/// - {@link JsonString} - Represents JSON strings -/// - {@link JsonNumber} - Represents JSON numbers -/// - {@link JsonBoolean} - Represents JSON booleans (true/false) -/// - {@link JsonNull} - Represents JSON null -/// - {@link JsonParseException} - Thrown when parsing invalid JSON -/// -/// ### Simple Parsing Example -/// -/// ```java -/// // Parse a JSON string -/// String jsonText = """ -/// { -/// "name": "Alice", -/// "age": 30, -/// "active": true -/// } -/// """; -/// -/// JsonValue value = Json.parse(jsonText); -/// JsonObject obj = (JsonObject) value; -/// -/// // Access values -/// String name = ((JsonString) obj.members().get("name")).value(); -/// int age = ((JsonNumber) obj.members().get("age")).toNumber().intValue(); -/// boolean active = ((JsonBoolean) obj.members().get("active")).value(); -/// ``` -/// -/// ### Record Mapping Example -/// -/// The API works seamlessly with Java records for domain modeling: -/// -/// ```java -/// // Define your domain model -/// record User(String name, String email, boolean active) {} -/// record Team(String teamName, List members) {} -/// -/// // Create domain objects -/// Team team = new Team("Engineering", List.of( -/// new User("Alice", "alice@example.com", true), -/// new User("Bob", "bob@example.com", false) -/// )); -/// -/// // Convert to JSON using Java collections -/// JsonValue teamJson = Json.fromUntyped(Map.of( -/// "teamName", team.teamName(), -/// "members", team.members().stream() -/// .map(u -> Map.of( -/// "name", u.name(), -/// "email", u.email(), -/// "active", u.active() -/// )) -/// .toList() -/// )); -/// -/// // Parse back to records -/// JsonObject parsed = (JsonObject) Json.parse(teamJson.toString()); -/// Team reconstructed = new Team( -/// ((JsonString) parsed.members().get("teamName")).value(), -/// ((JsonArray) parsed.members().get("members")).values().stream() -/// .map(v -> { -/// JsonObject member = (JsonObject) v; -/// return new User( -/// ((JsonString) member.members().get("name")).value(), -/// ((JsonString) member.members().get("email")).value(), -/// ((JsonBoolean) member.members().get("active")).value() -/// ); -/// }) -/// .toList() -/// ); -/// ``` -/// -/// ### REST API Response Example -/// -/// Build complex JSON structures programmatically: -/// -/// ```java -/// // Build a typical REST API response -/// JsonObject response = JsonObject.of(Map.of( -/// "status", JsonString.of("success"), -/// "data", JsonObject.of(Map.of( -/// "user", JsonObject.of(Map.of( -/// "id", JsonNumber.of(12345), -/// "name", JsonString.of("John Doe"), -/// "roles", JsonArray.of(List.of( -/// JsonString.of("admin"), -/// JsonString.of("user") -/// )) -/// )), -/// "timestamp", JsonNumber.of(System.currentTimeMillis()) -/// )), -/// "errors", JsonArray.of(List.of()) -/// )); -/// -/// // Pretty print the response -/// String formatted = Json.toDisplayString(response, 2); -/// ``` -/// -/// @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript -/// Object Notation (JSON) Data Interchange Format -/// @since 99 - +/** + * Provides APIs for parsing JSON text, retrieving JSON values in the text, and + * generating JSON text. + * + *

Parsing JSON documents

+ * + * Parsing produces a {@code JsonValue} from JSON text and is done using either + * {@link Json#parse(java.lang.String)} or {@link Json#parse(char[])}. A successful + * parse indicates that the JSON text adheres to the + * JSON grammar. + * The parsing APIs provided do not accept JSON text that contain JSON objects + * with duplicate names. + * + *

Retrieving JSON values

+ * + * Retrieving values from a JSON document involves two steps: first navigating + * the document structure using a chain of "access" methods, and then converting + * the result to the desired type using a "conversion" method. For example, + * {@snippet lang=java: + * var name = doc.get("foo").get("bar").element(0).string(); + * } + * By chaining access methods, the "foo" member is retrieved from the root object, + * then the "bar" member from "foo", followed by the element at index 0 from "bar". + * The navigation process leads to a leaf JSON string element. The final call to the + * {@code string()} conversion method returns the corresponding String object. For more + * details on these methods, see {@link JsonValue JsonValue}. + * + *

Generating JSON documents

+ * + * Generating JSON text is performed with either {@link + * JsonValue#toString()} or {@link Json#toDisplayString(JsonValue, int)}. + * These methods produce formatted String representations of a {@code JsonValue}. + * The returned text adheres to the JSON grammar defined in RFC 8259. + * {@code JsonValue.toString()} produces the most compact representation which does not + * include extra whitespaces or line-breaks, preferable for network transaction + * or storage. {@code Json.toDisplayString(JsonValue, int)} produces a text which + * is human friendly, preferable for debugging or logging. + * + * @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript + * Object Notation (JSON) Data Interchange Format + * @since 99 + */ package jdk.sandbox.java.util.json; diff --git a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java index 860358f..c1c7d6b 100644 --- a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java +++ b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java @@ -14,24 +14,24 @@ public class JsonParserTests { void testParseComplexJson() { JsonObject jsonObject = complexJsonObject(); - assertThat(((JsonString) jsonObject.members().get("name")).value()).isEqualTo("John Doe"); - assertThat(((JsonNumber) jsonObject.members().get("age")).toNumber().longValue()).isEqualTo(30L); - assertThat(((JsonBoolean) jsonObject.members().get("isStudent")).value()).isFalse(); + assertThat(((JsonString) jsonObject.members().get("name")).string()).isEqualTo("John Doe"); + assertThat(((JsonNumber) jsonObject.members().get("age")).toLong()).isEqualTo(30L); + assertThat(((JsonBoolean) jsonObject.members().get("isStudent")).bool()).isFalse(); JsonArray courses = (JsonArray) jsonObject.members().get("courses"); - assertThat(courses.values()).hasSize(2); + assertThat(courses.elements()).hasSize(2); - JsonObject course1 = (JsonObject) courses.values().getFirst(); - assertThat(((JsonString) course1.members().get("title")).value()).isEqualTo("History"); - assertThat(((JsonNumber) course1.members().get("credits")).toNumber().longValue()).isEqualTo(3L); + JsonObject course1 = (JsonObject) courses.elements().getFirst(); + assertThat(((JsonString) course1.members().get("title")).string()).isEqualTo("History"); + assertThat(((JsonNumber) course1.members().get("credits")).toLong()).isEqualTo(3L); - JsonObject course2 = (JsonObject) courses.values().get(1); - assertThat(((JsonString) course2.members().get("title")).value()).isEqualTo("Math"); - assertThat(((JsonNumber) course2.members().get("credits")).toNumber().longValue()).isEqualTo(4L); + JsonObject course2 = (JsonObject) courses.elements().get(1); + assertThat(((JsonString) course2.members().get("title")).string()).isEqualTo("Math"); + assertThat(((JsonNumber) course2.members().get("credits")).toLong()).isEqualTo(4L); JsonObject address = (JsonObject) jsonObject.members().get("address"); - assertThat(((JsonString) address.members().get("street")).value()).isEqualTo("123 Main St"); - assertThat(((JsonString) address.members().get("city")).value()).isEqualTo("Anytown"); + assertThat(((JsonString) address.members().get("street")).string()).isEqualTo("123 Main St"); + assertThat(((JsonString) address.members().get("city")).string()).isEqualTo("Anytown"); } private static JsonObject complexJsonObject() { diff --git a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java index ca88b80..73d5fe3 100644 --- a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java +++ b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java @@ -16,10 +16,10 @@ public class JsonPatternMatchingTests { private String identifyJsonValue(JsonValue jsonValue) { return switch (jsonValue) { case JsonObject o -> "Object with " + o.members().size() + " members"; - case JsonArray a -> "Array with " + a.values().size() + " elements"; - case JsonString s -> "String with value: " + s.value(); - case JsonNumber n -> "Number with value: " + n.toNumber(); - case JsonBoolean b -> "Boolean with value: " + b.value(); + case JsonArray a -> "Array with " + a.elements().size() + " elements"; + case JsonString s -> "String with value: " + s.string(); + case JsonNumber n -> "Number with value: " + n.toDouble(); + case JsonBoolean b -> "Boolean with value: " + b.bool(); case JsonNull ignored -> "Null"; }; } diff --git a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java index e7202d1..21acbd4 100644 --- a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java +++ b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java @@ -77,31 +77,31 @@ private Ecommerce toDomain(JsonValue jsonValue) { } Map members = jsonObject.members(); - String type = ((JsonString) members.get("type")).value(); + String type = ((JsonString) members.get("type")).string(); return switch (type) { case "order" -> { - String orderId = ((JsonString) members.get("orderId")).value(); + String orderId = ((JsonString) members.get("orderId")).string(); Customer customer = (Customer) toDomain(members.get("customer")); - List items = ((JsonArray) members.get("items")).values().stream() + List items = ((JsonArray) members.get("items")).elements().stream() .map(item -> (LineItem) toDomain(item)) .collect(Collectors.toList()); yield new Order(orderId, customer, items); } case "customer" -> { - String name = ((JsonString) members.get("name")).value(); - String email = ((JsonString) members.get("email")).value(); + String name = ((JsonString) members.get("name")).string(); + String email = ((JsonString) members.get("email")).string(); yield new Customer(name, email); } case "lineItem" -> { Product product = (Product) toDomain(members.get("product")); - int quantity = ((JsonNumber) members.get("quantity")).toNumber().intValue(); + int quantity = (int) ((JsonNumber) members.get("quantity")).toLong(); yield new LineItem(product, quantity); } case "product" -> { - String sku = ((JsonString) members.get("sku")).value(); - String name = ((JsonString) members.get("name")).value(); - double price = ((JsonNumber) members.get("price")).toNumber().doubleValue(); + String sku = ((JsonString) members.get("sku")).string(); + String name = ((JsonString) members.get("name")).string(); + double price = ((JsonNumber) members.get("price")).toDouble(); yield new Product(sku, name, price); } default -> throw new IllegalStateException("Unexpected value: " + type); diff --git a/json-java21/src/test/java/jdk/sandbox/java/util/json/EscapedKeyBugTest.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/EscapedKeyBugTest.java index 49dec75..b18d261 100644 --- a/json-java21/src/test/java/jdk/sandbox/java/util/json/EscapedKeyBugTest.java +++ b/json-java21/src/test/java/jdk/sandbox/java/util/json/EscapedKeyBugTest.java @@ -51,8 +51,8 @@ public void testEscapedCharactersInKeys() { JsonObject obj = (JsonObject) result; // Verify both keys are parsed correctly - assertEquals(1L, ((JsonNumber) obj.members().get("foo\nbar")).toNumber()); - assertEquals(2L, ((JsonNumber) obj.members().get("foo\tbar")).toNumber()); + assertEquals(1L, ((JsonNumber) obj.members().get("foo\nbar")).toLong()); + assertEquals(2L, ((JsonNumber) obj.members().get("foo\tbar")).toLong()); } @Test @@ -67,7 +67,7 @@ public void testEscapedQuoteInKey() { JsonObject obj = (JsonObject) result; // Verify key with escaped quote is parsed correctly - assertEquals(1L, ((JsonNumber) obj.members().get("foo\"bar")).toNumber()); + assertEquals(1L, ((JsonNumber) obj.members().get("foo\"bar")).toLong()); } @Test @@ -82,7 +82,7 @@ public void testEscapedBackslashInKey() { JsonObject obj = (JsonObject) result; // Verify key with escaped backslash is parsed correctly - assertEquals(1L, ((JsonNumber) obj.members().get("foo\\bar")).toNumber()); + assertEquals(1L, ((JsonNumber) obj.members().get("foo\\bar")).toLong()); } @Test @@ -97,6 +97,6 @@ public void testMultipleEscapedCharactersInKey() { JsonObject obj = (JsonObject) result; // Verify key with multiple escaped characters is parsed correctly - assertEquals(1L, ((JsonNumber) obj.members().get("foo\n\t\"bar")).toNumber()); + assertEquals(1L, ((JsonNumber) obj.members().get("foo\n\t\"bar")).toLong()); } } diff --git a/json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java deleted file mode 100644 index a981ce9..0000000 --- a/json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java +++ /dev/null @@ -1,236 +0,0 @@ -package jdk.sandbox.java.util.json; - -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class JsonTypedUntypedTests { - - @Test - void testFromUntypedWithSimpleTypes() { - // Test string - JsonValue jsonString = Json.fromUntyped("hello"); - assertThat(jsonString).isInstanceOf(JsonString.class); - assertThat(((JsonString) jsonString).value()).isEqualTo("hello"); - - // Test integer - JsonValue jsonInt = Json.fromUntyped(42); - assertThat(jsonInt).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonInt).toNumber()).isEqualTo(42L); - - // Test long - JsonValue jsonLong = Json.fromUntyped(42L); - assertThat(jsonLong).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonLong).toNumber()).isEqualTo(42L); - - // Test double - JsonValue jsonDouble = Json.fromUntyped(3.14); - assertThat(jsonDouble).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonDouble).toNumber()).isEqualTo(3.14); - - // Test boolean - JsonValue jsonBool = Json.fromUntyped(true); - assertThat(jsonBool).isInstanceOf(JsonBoolean.class); - assertThat(((JsonBoolean) jsonBool).value()).isTrue(); - - // Test null - JsonValue jsonNull = Json.fromUntyped(null); - assertThat(jsonNull).isInstanceOf(JsonNull.class); - } - - @Test - void testFromUntypedWithBigNumbers() { - // Test BigInteger - BigInteger bigInt = new BigInteger("123456789012345678901234567890"); - JsonValue jsonBigInt = Json.fromUntyped(bigInt); - assertThat(jsonBigInt).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonBigInt).toNumber()).isEqualTo(bigInt); - - // Test BigDecimal - BigDecimal bigDec = new BigDecimal("123456789012345678901234567890.123456789"); - JsonValue jsonBigDec = Json.fromUntyped(bigDec); - assertThat(jsonBigDec).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonBigDec).toNumber()).isEqualTo(bigDec); - } - - @Test - void testFromUntypedWithCollections() { - // Test List - List list = List.of("item1", 42, true); - JsonValue jsonArray = Json.fromUntyped(list); - assertThat(jsonArray).isInstanceOf(JsonArray.class); - JsonArray array = (JsonArray) jsonArray; - assertThat(array.values()).hasSize(3); - assertThat(((JsonString) array.values().get(0)).value()).isEqualTo("item1"); - assertThat(((JsonNumber) array.values().get(1)).toNumber()).isEqualTo(42L); - assertThat(((JsonBoolean) array.values().get(2)).value()).isTrue(); - - // Test Map - Map map = Map.of("name", "John", "age", 30, "active", true); - JsonValue jsonObject = Json.fromUntyped(map); - assertThat(jsonObject).isInstanceOf(JsonObject.class); - JsonObject obj = (JsonObject) jsonObject; - assertThat(((JsonString) obj.members().get("name")).value()).isEqualTo("John"); - assertThat(((JsonNumber) obj.members().get("age")).toNumber()).isEqualTo(30L); - assertThat(((JsonBoolean) obj.members().get("active")).value()).isTrue(); - } - - @Test - void testFromUntypedWithNestedStructures() { - Map nested = Map.of( - "user", Map.of("name", "John", "age", 30), - "scores", List.of(85, 92, 78), - "active", true - ); - - JsonValue json = Json.fromUntyped(nested); - assertThat(json).isInstanceOf(JsonObject.class); - - JsonObject root = (JsonObject) json; - JsonObject user = (JsonObject) root.members().get("user"); - assertThat(((JsonString) user.members().get("name")).value()).isEqualTo("John"); - - JsonArray scores = (JsonArray) root.members().get("scores"); - assertThat(scores.values()).hasSize(3); - assertThat(((JsonNumber) scores.values().getFirst()).toNumber()).isEqualTo(85L); - } - - @Test - void testFromUntypedWithJsonValue() { - // If input is already a JsonValue, return as-is - JsonString original = JsonString.of("test"); - JsonValue result = Json.fromUntyped(original); - assertThat(result).isSameAs(original); - } - - @Test - void testFromUntypedWithInvalidTypes() { - // Test with unsupported type - assertThatThrownBy(() -> Json.fromUntyped(new StringBuilder("test"))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("StringBuilder is not a recognized type"); - } - - @Test - void testFromUntypedWithNonStringMapKey() { - // Test map with non-string key - Map invalidMap = Map.of(123, "value"); - assertThatThrownBy(() -> Json.fromUntyped(invalidMap)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("The key '123' is not a String"); - } - - @Test - void testToUntypedWithSimpleTypes() { - // Test string - Object str = Json.toUntyped(JsonString.of("hello")); - assertThat(str).isEqualTo("hello"); - - // Test number - Object num = Json.toUntyped(JsonNumber.of(42)); - assertThat(num).isEqualTo(42L); - - // Test boolean - Object bool = Json.toUntyped(JsonBoolean.of(true)); - assertThat(bool).isEqualTo(true); - - // Test null - Object nullVal = Json.toUntyped(JsonNull.of()); - assertThat(nullVal).isNull(); - } - - @Test - void testToUntypedWithCollections() { - // Test array - JsonArray array = JsonArray.of(List.of( - JsonString.of("item1"), - JsonNumber.of(42), - JsonBoolean.of(true) - )); - Object result = Json.toUntyped(array); - assertThat(result).isInstanceOf(List.class); - @SuppressWarnings("unchecked") - List list = (List) result; - assertThat(list).containsExactly("item1", 42L, true); - - // Test object - JsonObject obj = JsonObject.of(Map.of( - "name", JsonString.of("John"), - "age", JsonNumber.of(30), - "active", JsonBoolean.of(true) - )); - Object objResult = Json.toUntyped(obj); - assertThat(objResult).isInstanceOf(Map.class); - Map map = (Map) objResult; - assert map != null; - assertThat(map.get("name")).isEqualTo("John"); - assertThat(map.get("age")).isEqualTo(30L); - assertThat(map.get("active")).isEqualTo(true); - } - - @Test - void testRoundTripConversion() { - // Create complex nested structure - Map original = Map.of( - "user", Map.of( - "name", "John Doe", - "age", 30, - "email", "john@example.com" - ), - "scores", List.of(85.5, 92.0, 78.3), - "active", true, - "metadata", Map.of( - "created", "2024-01-01", - "tags", List.of("vip", "premium") - ) - ); - - // Convert to JsonValue and back - JsonValue json = Json.fromUntyped(original); - Object reconstructed = Json.toUntyped(json); - - // Verify structure is preserved - assertThat(reconstructed).isInstanceOf(Map.class); - Map resultMap = (Map) reconstructed; - - assert resultMap != null; - Map user = (Map) resultMap.get("user"); - assertThat(user.get("name")).isEqualTo("John Doe"); - assertThat(user.get("age")).isEqualTo(30L); - - @SuppressWarnings("unchecked") - List scores = (List) resultMap.get("scores"); - assertThat(scores).containsExactly(85.5, 92.0, 78.3); - - Map metadata = (Map) resultMap.get("metadata"); - @SuppressWarnings("unchecked") - List tags = (List) metadata.get("tags"); - assertThat(tags).containsExactly("vip", "premium"); - } - - @Test - void testToUntypedPreservesOrder() { - // JsonObject should preserve insertion order - JsonObject obj = JsonObject.of(Map.of( - "z", JsonString.of("last"), - "a", JsonString.of("first"), - "m", JsonString.of("middle") - )); - - Object result = Json.toUntyped(obj); - assertThat(result).isInstanceOf(Map.class); - - // The order might not be preserved with Map.of(), so let's just verify contents - @SuppressWarnings("unchecked") - Map map = (Map) result; - assertThat(map).containsEntry("z", "last") - .containsEntry("a", "first") - .containsEntry("m", "middle"); - } -} \ No newline at end of file diff --git a/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java index ada8173..1899aad 100644 --- a/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java +++ b/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java @@ -18,8 +18,8 @@ void quickStartExample() { assertThat(value).isInstanceOf(JsonObject.class); JsonObject obj = (JsonObject) value; - assertThat(((JsonString) obj.members().get("name")).value()).isEqualTo("Alice"); - assertThat(((JsonNumber) obj.members().get("age")).toNumber()).isEqualTo(30L); + assertThat(((JsonString) obj.members().get("name")).string()).isEqualTo("Alice"); + assertThat(((JsonNumber) obj.members().get("age")).toLong()).isEqualTo(30L); String roundTrip = value.toString(); assertThat(roundTrip).isEqualTo(jsonString); @@ -40,37 +40,37 @@ void recordMappingExample() { new User("Bob", "bob@example.com", false) )); - // Convert records to JSON using untyped conversion - JsonValue teamJson = Json.fromUntyped(Map.of( - "teamName", team.teamName(), - "members", team.members().stream() - .map(u -> Map.of( - "name", u.name(), - "email", u.email(), - "active", u.active() - )) - .toList() + // Convert records to JSON using typed factories + JsonValue teamJson = JsonObject.of(Map.of( + "teamName", JsonString.of(team.teamName()), + "members", JsonArray.of(team.members().stream() + .map(u -> JsonObject.of(Map.of( + "name", JsonString.of(u.name()), + "email", JsonString.of(u.email()), + "active", JsonBoolean.of(u.active()) + ))) + .toList()) )); // Verify the JSON structure assertThat(teamJson).isInstanceOf(JsonObject.class); JsonObject teamObj = (JsonObject) teamJson; - assertThat(((JsonString) teamObj.members().get("teamName")).value()).isEqualTo("Engineering"); + assertThat(((JsonString) teamObj.members().get("teamName")).string()).isEqualTo("Engineering"); JsonArray members = (JsonArray) teamObj.members().get("members"); - assertThat(members.values()).hasSize(2); + assertThat(members.elements()).hasSize(2); // Parse JSON back to records JsonObject parsed = (JsonObject) Json.parse(teamJson.toString()); Team reconstructed = new Team( - ((JsonString) parsed.members().get("teamName")).value(), - ((JsonArray) parsed.members().get("members")).values().stream() + ((JsonString) parsed.members().get("teamName")).string(), + ((JsonArray) parsed.members().get("members")).elements().stream() .map(v -> { JsonObject member = (JsonObject) v; return new User( - ((JsonString) member.members().get("name")).value(), - ((JsonString) member.members().get("email")).value(), - ((JsonBoolean) member.members().get("active")).value() + ((JsonString) member.members().get("name")).string(), + ((JsonString) member.members().get("email")).string(), + ((JsonBoolean) member.members().get("active")).bool() ); }) .toList() @@ -106,19 +106,19 @@ void builderPatternExample() { )); // Verify structure - assertThat(((JsonString) response.members().get("status")).value()).isEqualTo("success"); + assertThat(((JsonString) response.members().get("status")).string()).isEqualTo("success"); JsonObject data = (JsonObject) response.members().get("data"); JsonObject user = (JsonObject) data.members().get("user"); - assertThat(((JsonNumber) user.members().get("id")).toNumber()).isEqualTo(12345L); - assertThat(((JsonString) user.members().get("name")).value()).isEqualTo("John Doe"); + assertThat(((JsonNumber) user.members().get("id")).toLong()).isEqualTo(12345L); + assertThat(((JsonString) user.members().get("name")).string()).isEqualTo("John Doe"); JsonArray roles = (JsonArray) user.members().get("roles"); - assertThat(roles.values()).hasSize(2); - assertThat(((JsonString) roles.values().getFirst()).value()).isEqualTo("admin"); + assertThat(roles.elements()).hasSize(2); + assertThat(((JsonString) roles.elements().getFirst()).string()).isEqualTo("admin"); JsonArray errors = (JsonArray) response.members().get("errors"); - assertThat(errors.values()).isEmpty(); + assertThat(errors.elements()).isEmpty(); } @Test @@ -136,10 +136,10 @@ void streamingProcessingExample() { // Process a large array of records JsonArray items = (JsonArray) Json.parse(largeJsonArray); - List activeUserEmails = items.values().stream() + List activeUserEmails = items.elements().stream() .map(v -> (JsonObject) v) - .filter(obj -> ((JsonBoolean) obj.members().get("active")).value()) - .map(obj -> ((JsonString) obj.members().get("email")).value()) + .filter(obj -> ((JsonBoolean) obj.members().get("active")).bool()) + .map(obj -> ((JsonString) obj.members().get("email")).string()) .toList(); // Verify we got only active users @@ -164,41 +164,6 @@ void errorHandlingExample() { .hasMessageContaining("Expecting a JSON Object member name"); } - @Test - void typeConversionExample() { - // Using fromUntyped and toUntyped for complex structures - Map config = Map.of( - "server", Map.of( - "host", "localhost", - "port", 8080, - "ssl", true - ), - "features", List.of("auth", "logging", "metrics"), - "maxConnections", 1000 - ); - - // Convert to JSON - JsonValue json = Json.fromUntyped(config); - - // Convert back to Java types - @SuppressWarnings("unchecked") - Map restored = (Map) Json.toUntyped(json); - - // Verify round-trip conversion - assert restored != null; - @SuppressWarnings("unchecked") - Map server = (Map) restored.get("server"); - assertThat(server.get("host")).isEqualTo("localhost"); - assertThat(server.get("port")).isEqualTo(8080L); // Note: integers become Long - assertThat(server.get("ssl")).isEqualTo(true); - - @SuppressWarnings("unchecked") - List features = (List) restored.get("features"); - assertThat(features).containsExactly("auth", "logging", "metrics"); - - assertThat(restored.get("maxConnections")).isEqualTo(1000L); - } - @Test void displayFormattingExample() { // Create a structured JSON From c594833e9e7d43786ee6eb9276aac2ec15638354 Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:03:03 +0000 Subject: [PATCH 3/4] Update expected test count after removing JsonTypedUntypedTests The upstream API removed fromUntyped/toUntyped methods, so we deleted the test file that tested those methods (12 tests removed: 509 -> 497). --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b31351c..c22cf59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: for k in totals: totals[k]+=int(r.get(k,'0')) except Exception: pass - exp_tests=509 + exp_tests=497 exp_skipped=0 if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped: print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}") From a66c2de117bb1513c22803e5c302e75f7650f53f Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:06:34 +0000 Subject: [PATCH 4/4] Add LazyConstant tests and update expected test count Added 10 tests for LazyConstant.java polyfill covering: - Lazy initialization behavior - Thread safety with concurrent access - Caching/memoization - Exception propagation - Usage in JSON parsing context Test count: 509 -> 507 (removed 12 fromUntyped/toUntyped tests, added 10 LazyConstant tests) --- .github/workflows/ci.yml | 2 +- .../internal/util/json/LazyConstantTest.java | 251 ++++++++++++++++++ 2 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 json-java21/src/test/java/jdk/sandbox/internal/util/json/LazyConstantTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c22cf59..d799570 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: for k in totals: totals[k]+=int(r.get(k,'0')) except Exception: pass - exp_tests=497 + exp_tests=507 exp_skipped=0 if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped: print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}") diff --git a/json-java21/src/test/java/jdk/sandbox/internal/util/json/LazyConstantTest.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/LazyConstantTest.java new file mode 100644 index 0000000..8c1d28e --- /dev/null +++ b/json-java21/src/test/java/jdk/sandbox/internal/util/json/LazyConstantTest.java @@ -0,0 +1,251 @@ +package jdk.sandbox.internal.util.json; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import static org.assertj.core.api.Assertions.assertThat; + +/// Tests for [LazyConstant] polyfill that provides thread-safe lazy initialization. +class LazyConstantTest { + + private static final Logger LOG = Logger.getLogger(LazyConstantTest.class.getName()); + + @Test + void testLazyInitialization() { + LOG.info("Running testLazyInitialization"); + + AtomicInteger computeCount = new AtomicInteger(0); + + LazyConstant lazy = LazyConstant.of(() -> { + computeCount.incrementAndGet(); + return "computed value"; + }); + + // Supplier should not be called yet + assertThat(computeCount.get()).isEqualTo(0); + + // First get() should compute + String value = lazy.get(); + assertThat(value).isEqualTo("computed value"); + assertThat(computeCount.get()).isEqualTo(1); + + // Second get() should return cached value without recomputing + String value2 = lazy.get(); + assertThat(value2).isEqualTo("computed value"); + assertThat(computeCount.get()).isEqualTo(1); + } + + @Test + void testReturnsComputedValue() { + LOG.info("Running testReturnsComputedValue"); + + LazyConstant lazy = LazyConstant.of(() -> 42); + + assertThat(lazy.get()).isEqualTo(42); + assertThat(lazy.get()).isEqualTo(42); + } + + @Test + void testWithComplexObject() { + LOG.info("Running testWithComplexObject"); + + LazyConstant lazy = LazyConstant.of(() -> { + StringBuilder sb = new StringBuilder(); + sb.append("hello"); + sb.append(" "); + sb.append("world"); + return sb; + }); + + StringBuilder result = lazy.get(); + assertThat(result.toString()).isEqualTo("hello world"); + + // Should return the same instance + assertThat(lazy.get()).isSameAs(result); + } + + @Test + void testThreadSafety() throws InterruptedException { + LOG.info("Running testThreadSafety"); + + AtomicInteger computeCount = new AtomicInteger(0); + + LazyConstant lazy = LazyConstant.of(() -> { + computeCount.incrementAndGet(); + // Simulate some computation time + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "thread-safe value"; + }); + + int numThreads = 10; + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(numThreads); + + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + + for (int i = 0; i < numThreads; i++) { + executor.submit(() -> { + try { + startLatch.await(); // Wait for all threads to be ready + String value = lazy.get(); + assertThat(value).isEqualTo("thread-safe value"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + doneLatch.countDown(); + } + }); + } + + // Release all threads at once + startLatch.countDown(); + + // Wait for all threads to complete + doneLatch.await(); + executor.shutdown(); + + // Supplier should only have been called once despite concurrent access + assertThat(computeCount.get()).isEqualTo(1); + } + + @Test + void testMultipleInstances() { + LOG.info("Running testMultipleInstances"); + + AtomicInteger counter1 = new AtomicInteger(0); + AtomicInteger counter2 = new AtomicInteger(0); + + LazyConstant lazy1 = LazyConstant.of(() -> { + counter1.incrementAndGet(); + return "value1"; + }); + + LazyConstant lazy2 = LazyConstant.of(() -> { + counter2.incrementAndGet(); + return "value2"; + }); + + assertThat(lazy1.get()).isEqualTo("value1"); + assertThat(lazy2.get()).isEqualTo("value2"); + + assertThat(counter1.get()).isEqualTo(1); + assertThat(counter2.get()).isEqualTo(1); + } + + @Test + void testSupplierExceptionPropagates() { + LOG.info("Running testSupplierExceptionPropagates"); + + LazyConstant lazy = LazyConstant.of(() -> { + throw new RuntimeException("computation failed"); + }); + + try { + lazy.get(); + assertThat(false).as("Should have thrown exception").isTrue(); + } catch (RuntimeException e) { + assertThat(e.getMessage()).isEqualTo("computation failed"); + } + } + + @Test + void testWithExpensiveComputation() { + LOG.info("Running testWithExpensiveComputation"); + + AtomicInteger computeCount = new AtomicInteger(0); + + // Simulates expensive computation like parsing a large string + LazyConstant lazy = LazyConstant.of(() -> { + computeCount.incrementAndGet(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + sb.append(i).append(","); + } + return sb.toString(); + }); + + // Access multiple times + for (int i = 0; i < 100; i++) { + String value = lazy.get(); + assertThat(value).startsWith("0,1,2,"); + } + + // Should only compute once + assertThat(computeCount.get()).isEqualTo(1); + } + + @Test + void testCachesValueAcrossMultipleGets() { + LOG.info("Running testCachesValueAcrossMultipleGets"); + + AtomicInteger callCount = new AtomicInteger(0); + + LazyConstant lazy = LazyConstant.of(() -> { + callCount.incrementAndGet(); + return new Object(); // Each call would create a new instance + }); + + Object first = lazy.get(); + Object second = lazy.get(); + Object third = lazy.get(); + + // All should be the same instance + assertThat(first).isSameAs(second); + assertThat(second).isSameAs(third); + + // Supplier called only once + assertThat(callCount.get()).isEqualTo(1); + } + + @Test + void testUsedInJsonParsingContext() { + LOG.info("Running testUsedInJsonParsingContext"); + + // Simulates how LazyConstant is used in JsonStringImpl/JsonNumberImpl + // where the string representation is computed lazily from a char array + + char[] doc = "\"hello world\"".toCharArray(); + int start = 0; + int end = doc.length; + + LazyConstant lazyString = LazyConstant.of(() -> + new String(doc, start, end - start) + ); + + assertThat(lazyString.get()).isEqualTo("\"hello world\""); + assertThat(lazyString.get()).isEqualTo("\"hello world\""); + } + + @Test + void testMemoizesNullUnsupported() { + LOG.info("Running testMemoizesNullUnsupported"); + + // Note: Current implementation doesn't support null values + // (null is used as the "not yet computed" sentinel) + // This documents the current behavior + + AtomicInteger callCount = new AtomicInteger(0); + + LazyConstant lazy = LazyConstant.of(() -> { + callCount.incrementAndGet(); + return null; // Returns null + }); + + // Each call will recompute because null can't be cached + lazy.get(); + lazy.get(); + + // This shows the limitation - null values cause recomputation + // In practice, JSON parsing doesn't return null from suppliers + assertThat(callCount.get()).isGreaterThanOrEqualTo(2); + } +}