diff --git a/json-java21-api-tracker/pom.xml b/json-java21-api-tracker/pom.xml index eb03e82..9f631ac 100644 --- a/json-java21-api-tracker/pom.xml +++ b/json-java21-api-tracker/pom.xml @@ -43,39 +43,8 @@ - - - relaxed - - - - org.apache.maven.plugins - maven-compiler-plugin - - - -Xlint:all - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${maven.compiler.release} - - --enable-preview - -Xlint:all - -Werror - - - org.apache.maven.plugins maven-surefire-plugin 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 13b8c33..2596c39 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 @@ -15,25 +15,15 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; -import java.util.ArrayList; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.tools.DiagnosticCollector; -import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; -import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; import com.sun.source.tree.ClassTree; @@ -45,7 +35,7 @@ import com.sun.source.util.TreePathScanner; /// API Tracker module for comparing local and upstream JSON APIs -/// +/// /// This module provides functionality to: /// - Discover local JSON API classes via reflection /// - Fetch corresponding upstream sources from GitHub @@ -55,40 +45,40 @@ /// Modular design supports different extraction strategies: /// - Binary reflection for quick class introspection /// - Source parsing for accurate parameter names and signatures -/// +/// /// All functionality is exposed as static methods following functional programming principles public sealed interface ApiTracker permits ApiTracker.Nothing { - + /// Local source root for source-based extraction - static final String LOCAL_SOURCE_ROOT = "json-java21/src/main/java"; - + String LOCAL_SOURCE_ROOT = "json-java21/src/main/java"; + /// Empty enum to seal the interface - no instances allowed enum Nothing implements ApiTracker {} - + // Package-private logger shared across the module - static final Logger LOGGER = Logger.getLogger(ApiTracker.class.getName()); - + Logger LOGGER = Logger.getLogger(ApiTracker.class.getName()); + // Cache for HTTP responses to avoid repeated fetches - static final Map FETCH_CACHE = new ConcurrentHashMap<>(); - + Map FETCH_CACHE = new ConcurrentHashMap<>(); + // GitHub base URL for upstream sources - static final String GITHUB_BASE_URL = "https://raw.githubusercontent.com/openjdk/jdk-sandbox/refs/heads/json/src/java.base/share/classes/"; - + String GITHUB_BASE_URL = "https://raw.githubusercontent.com/openjdk/jdk-sandbox/refs/heads/json/src/java.base/share/classes/"; + /// Fetches content from a URL static String fetchFromUrl(String url) { final var httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); - + try { final var request = HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(30)) .GET() .build(); - + final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - + if (response.statusCode() == 200) { return response.body(); } else if (response.statusCode() == 404) { @@ -100,29 +90,29 @@ static String fetchFromUrl(String url) { return "FETCH_ERROR: " + e.getMessage(); } } - + /// Discovers all classes in the local JSON API packages /// @return sorted set of classes from jdk.sandbox.java.util.json and jdk.sandbox.internal.util.json static Set> discoverLocalJsonClasses() { LOGGER.info("Starting class discovery for JSON API packages"); - final var classes = new TreeSet>((a, b) -> a.getName().compareTo(b.getName())); - + final var classes = new TreeSet>(Comparator.comparing(Class::getName)); + // Packages to scan - only public API, not internal implementation final var packages = List.of( "jdk.sandbox.java.util.json" ); - + final var classLoader = Thread.currentThread().getContextClassLoader(); - + for (final var packageName : packages) { try { final var path = packageName.replace('.', '/'); final var resources = classLoader.getResources(path); - + while (resources.hasMoreElements()) { final var url = resources.nextElement(); LOGGER.fine(() -> "Scanning resource: " + url); - + if ("file".equals(url.getProtocol())) { // Handle directory scanning scanDirectory(new java.io.File(url.toURI()), packageName, classes); @@ -135,28 +125,28 @@ static Set> discoverLocalJsonClasses() { LOGGER.log(Level.WARNING, "Error scanning package: " + packageName, e); } } - - LOGGER.info("Discovered " + classes.size() + " classes in JSON API packages: " + + + LOGGER.info("Discovered " + classes.size() + " classes in JSON API packages: " + classes.stream().map(Class::getName).sorted().collect(Collectors.joining(", "))); return Collections.unmodifiableSet(classes); } - + /// Scans a directory for class files static void scanDirectory(java.io.File directory, String packageName, Set> classes) { if (!directory.exists() || !directory.isDirectory()) { return; } - + final var files = directory.listFiles(); if (files == null) { return; } - + for (final var file : files) { if (file.isDirectory()) { scanDirectory(file, packageName + "." + file.getName(), classes); } else if (file.getName().endsWith(".class") && !file.getName().contains("$")) { - final var className = packageName + '.' + + final var className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6); try { final var clazz = Class.forName(className); @@ -168,7 +158,7 @@ static void scanDirectory(java.io.File directory, String packageName, Set> classes) { try { @@ -177,25 +167,25 @@ static void scanJar(java.net.URL jarUrl, String packageName, Set> class if (exclamation < 0) { return; } - + final var jarFilePath = jarPath.substring(5, exclamation); // Remove "file:" final var packagePath = packageName.replace('.', '/'); - + try (final var jarFile = new java.util.jar.JarFile(jarFilePath)) { final var entries = jarFile.entries(); - + while (entries.hasMoreElements()) { final var entry = entries.nextElement(); final var entryName = entry.getName(); - - if (entryName.startsWith(packagePath) && - entryName.endsWith(".class") && + + if (entryName.startsWith(packagePath) && + entryName.endsWith(".class") && !entryName.contains("$")) { - + final var className = entryName .substring(0, entryName.length() - 6) .replace('/', '.'); - + try { final var clazz = Class.forName(className); classes.add(clazz); @@ -210,44 +200,44 @@ static void scanJar(java.net.URL jarUrl, String packageName, Set> class LOGGER.log(Level.WARNING, "Error scanning JAR: " + jarUrl, e); } } - + /// Fetches upstream source files from GitHub for the given local classes /// @param localClasses set of local classes to fetch upstream sources for /// @return map of className to source code (or error message if fetch failed) static Map fetchUpstreamSources(Set> localClasses) { Objects.requireNonNull(localClasses, "localClasses must not be null"); LOGGER.info("Fetching upstream sources for " + localClasses.size() + " classes"); - + final var results = new LinkedHashMap(); final var httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); - + for (final var clazz : localClasses) { final var className = clazz.getName(); final var cachedSource = FETCH_CACHE.get(className); - + if (cachedSource != null) { LOGGER.fine(() -> "Using cached source for: " + className); results.put(className, cachedSource); continue; } - + // Map package name from jdk.sandbox.* to standard java.* final var upstreamPath = mapToUpstreamPath(className); final var url = GITHUB_BASE_URL + upstreamPath; - + LOGGER.info("Fetching upstream source: " + url); - + try { final var request = HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(30)) .GET() .build(); - + final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - + if (response.statusCode() == 200) { final var body = response.body(); FETCH_CACHE.put(className, body); @@ -268,10 +258,10 @@ static Map fetchUpstreamSources(Set> localClasses) { LOGGER.info("Fetch error for " + className + " at " + url + ": " + e.getMessage()); } } - + return Collections.unmodifiableMap(results); } - + /// Maps local class name to upstream GitHub path static String mapToUpstreamPath(String className) { // Remove jdk.sandbox prefix and map to standard packages @@ -279,10 +269,10 @@ static String mapToUpstreamPath(String className) { .replace("jdk.sandbox.java.util.json", "java/util/json") .replace("jdk.sandbox.internal.util.json", "jdk/internal/util/json") .replace('.', '/'); - + return path + ".java"; } - + /// Extracts local API from source file static JsonObject extractLocalApiFromSource(String className) { final var path = LOCAL_SOURCE_ROOT + "/" + className.replace('.', '/') + ".java"; @@ -296,7 +286,7 @@ static JsonObject extractLocalApiFromSource(String className) { )); } } - + /// Extracts public API from source code using compiler parsing /// @param sourceCode the source code to parse /// @param className the expected class name @@ -304,10 +294,10 @@ static JsonObject extractLocalApiFromSource(String className) { static JsonObject extractApiFromSource(String sourceCode, String className) { Objects.requireNonNull(sourceCode, "sourceCode must not be null"); Objects.requireNonNull(className, "className must not be null"); - + // Check for fetch errors - if (sourceCode.startsWith("NOT_FOUND:") || - sourceCode.startsWith("HTTP_ERROR:") || + if (sourceCode.startsWith("NOT_FOUND:") || + sourceCode.startsWith("HTTP_ERROR:") || sourceCode.startsWith("FETCH_ERROR:")) { final var errorMap = Map.of( "error", JsonString.of(sourceCode), @@ -315,9 +305,9 @@ static JsonObject extractApiFromSource(String sourceCode, String className) { ); return JsonObject.of(errorMap); } - + LOGGER.info("Extracting upstream API for: " + className); - + final var compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) { return JsonObject.of(Map.of( @@ -325,21 +315,21 @@ static JsonObject extractApiFromSource(String sourceCode, String className) { "className", JsonString.of(className) )); } - + final var diagnostics = new DiagnosticCollector(); final var fileManager = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8); - + try { // Extract simple class name from fully qualified name final var simpleClassName = className.substring(className.lastIndexOf('.') + 1); - + // Create compilation units final var compilationUnits = new ArrayList(); compilationUnits.add(new InMemoryJavaFileObject(className, sourceCode)); - + // Add minimal stubs for common dependencies addCommonStubs(compilationUnits); - + // Parse-only compilation with relaxed settings final var options = List.of( "-proc:none", @@ -348,7 +338,7 @@ static JsonObject extractApiFromSource(String sourceCode, String className) { "--enable-preview", "--release", "24" ); - + final var task = (JavacTask) compiler.getTask( null, fileManager, @@ -357,9 +347,9 @@ static JsonObject extractApiFromSource(String sourceCode, String className) { null, compilationUnits ); - + final var trees = task.parse(); - + // Extract API using visitor for (final var tree : trees) { final var fileName = tree.getSourceFile().getName(); @@ -369,13 +359,13 @@ static JsonObject extractApiFromSource(String sourceCode, String className) { return visitor.getExtractedApi(); } } - + // If we get here, parsing failed return JsonObject.of(Map.of( "error", JsonString.of("Failed to parse source"), "className", JsonString.of(className) )); - + } catch (Exception e) { LOGGER.log(Level.WARNING, "Error parsing upstream source for " + className, e); return JsonObject.of(Map.of( @@ -390,7 +380,7 @@ static JsonObject extractApiFromSource(String sourceCode, String className) { } } } - + /// Adds common stub dependencies for JSON API parsing static void addCommonStubs(List compilationUnits) { // PreviewFeature annotation stub @@ -404,20 +394,20 @@ static void addCommonStubs(List compilationUnits) { enum Feature { JSON } } """)); - + // JsonValue base interface stub compilationUnits.add(new InMemoryJavaFileObject("java.util.json.JsonValue", """ package java.util.json; public sealed interface JsonValue permits JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull {} """)); - + // Basic JSON type stubs final var jsonTypes = List.of("JsonObject", "JsonArray", "JsonString", "JsonNumber", "JsonBoolean", "JsonNull"); for (final var type : jsonTypes) { compilationUnits.add(new InMemoryJavaFileObject("java.util.json." + type, "package java.util.json; public non-sealed interface " + type + " extends JsonValue {}")); } - + // Internal implementation stubs compilationUnits.add(new InMemoryJavaFileObject("jdk.internal.util.json.JsonObjectImpl", """ package jdk.internal.util.json; @@ -432,9 +422,9 @@ public JsonObjectImpl(Map map) {} } """)); } - + /// In-memory JavaFileObject for creating stub classes - static class InMemoryJavaFileObject extends SimpleJavaFileObject { + class InMemoryJavaFileObject extends SimpleJavaFileObject { private final String content; InMemoryJavaFileObject(String className, String content) { @@ -447,9 +437,9 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) { return content; } } - + /// Visitor to extract API information from AST - static class ApiExtractorVisitor extends TreePathScanner { + class ApiExtractorVisitor extends TreePathScanner { private final Map apiMap = new LinkedHashMap<>(); private final Map methodsMap = new LinkedHashMap<>(); private final Map fieldsMap = new LinkedHashMap<>(); @@ -467,13 +457,13 @@ public Void visitClass(ClassTree node, Void p) { // Basic class information apiMap.put("className", JsonString.of(node.getSimpleName().toString())); apiMap.put("modifiers", extractTreeModifiers(node.getModifiers())); - + // Type information final var kind = node.getKind(); apiMap.put("isInterface", JsonBoolean.of(kind == Tree.Kind.INTERFACE)); apiMap.put("isEnum", JsonBoolean.of(kind == Tree.Kind.ENUM)); apiMap.put("isRecord", JsonBoolean.of(kind == Tree.Kind.RECORD)); - + // Package name (from compilation unit) final var compilationUnit = getCurrentPath().getCompilationUnit(); final var packageTree = compilationUnit.getPackage(); @@ -482,13 +472,13 @@ public Void visitClass(ClassTree node, Void p) { } else { apiMap.put("packageName", JsonString.of("")); } - + // Check if sealed final var modifiers = node.getModifiers(); final var isSealed = modifiers.getFlags().stream() .anyMatch(m -> m.toString().equals("SEALED")); apiMap.put("isSealed", JsonBoolean.of(isSealed)); - + // Inheritance final var superTypes = new ArrayList(); if (node.getExtendsClause() != null) { @@ -498,12 +488,12 @@ public Void visitClass(ClassTree node, Void p) { .map(tree -> JsonString.of(extractSimpleName(tree.toString()))) .forEach(superTypes::add); apiMap.put("extends", JsonArray.of(superTypes)); - + // Permitted subclasses (approximation - would need full symbol resolution) if (isSealed) { apiMap.put("permits", JsonArray.of(List.of())); } - + return super.visitClass(node, p); } @@ -511,7 +501,7 @@ public Void visitClass(ClassTree node, Void p) { public Void visitMethod(MethodTree node, Void p) { // Check if public final var isPublic = isPublicMember(node.getModifiers()); - + if (isPublic) { final var methodInfo = new LinkedHashMap(); methodInfo.put("modifiers", extractTreeModifiers(node.getModifiers())); @@ -519,17 +509,17 @@ public Void visitMethod(MethodTree node, Void p) { node.getReturnType() != null ? node.getReturnType().toString() : "void"))); methodInfo.put("genericReturnType", JsonString.of( node.getReturnType() != null ? node.getReturnType().toString() : "void")); - + final var params = node.getParameters().stream() .map(param -> JsonString.of(extractSimpleName(param.getType().toString()) + " " + param.getName())) .collect(Collectors.toList()); methodInfo.put("parameters", JsonArray.of(params)); - + final var exceptions = node.getThrows().stream() .map(ex -> JsonString.of(extractSimpleName(ex.toString()))) .collect(Collectors.toList()); methodInfo.put("throws", JsonArray.of(exceptions)); - + // Handle constructors separately if (node.getName().toString().equals("")) { constructors.add(JsonObject.of(methodInfo)); @@ -537,36 +527,36 @@ public Void visitMethod(MethodTree node, Void p) { methodsMap.put(node.getName().toString(), JsonObject.of(methodInfo)); } } - + return super.visitMethod(node, p); } - @Override + @Override public Void visitVariable(VariableTree node, Void p) { // Only process fields (not method parameters or local variables) if (getCurrentPath().getParentPath().getLeaf().getKind() == Tree.Kind.CLASS) { final var isPublic = isPublicMember(node.getModifiers()); - + if (isPublic) { final var fieldInfo = new LinkedHashMap(); fieldInfo.put("modifiers", extractTreeModifiers(node.getModifiers())); fieldInfo.put("type", JsonString.of(extractSimpleName(node.getType().toString()))); fieldInfo.put("genericType", JsonString.of(node.getType().toString())); - + fieldsMap.put(node.getName().toString(), JsonObject.of(fieldInfo)); } } - + return super.visitVariable(node, p); } - + private JsonArray extractTreeModifiers(ModifiersTree modifiers) { final var modList = modifiers.getFlags().stream() .map(m -> JsonString.of(m.toString().toLowerCase())) .collect(Collectors.toList()); return JsonArray.of(modList); } - + private boolean isPublicMember(ModifiersTree modifiers) { // In interfaces, methods without private/default are implicitly public final var parent = getCurrentPath().getParentPath(); @@ -576,7 +566,7 @@ private boolean isPublicMember(ModifiersTree modifiers) { } return modifiers.getFlags().contains(javax.lang.model.element.Modifier.PUBLIC); } - + private String extractSimpleName(String typeName) { // Remove generic parameters and package prefixes var name = typeName; @@ -591,7 +581,7 @@ private String extractSimpleName(String typeName) { return name; } } - + /// Compares local and upstream APIs to identify differences /// @param local the local API structure /// @param upstream the upstream API structure @@ -599,23 +589,23 @@ private String extractSimpleName(String typeName) { static JsonObject compareApis(JsonObject local, JsonObject upstream) { Objects.requireNonNull(local, "local must not be null"); Objects.requireNonNull(upstream, "upstream must not be null"); - + final var diffMap = new LinkedHashMap(); - + // Extract class name safely final var localClassName = local.members().get("className"); - final var className = localClassName instanceof JsonString js ? + final var className = localClassName instanceof JsonString js ? js.value() : "Unknown"; - + diffMap.put("className", JsonString.of(className)); - + // Check for upstream errors if (upstream.members().containsKey("error")) { diffMap.put("status", JsonString.of("UPSTREAM_ERROR")); diffMap.put("error", upstream.members().get("error")); return JsonObject.of(diffMap); } - + // Check if status is NOT_IMPLEMENTED (from parsing) if (upstream.members().containsKey("status")) { final var status = ((JsonString) upstream.members().get("status")).value(); @@ -624,32 +614,32 @@ static JsonObject compareApis(JsonObject local, JsonObject upstream) { return JsonObject.of(diffMap); } } - + // Perform detailed comparison final var differences = new ArrayList(); var hasChanges = false; - + // Compare basic class attributes hasChanges |= compareAttribute("isInterface", local, upstream, differences); hasChanges |= compareAttribute("isEnum", local, upstream, differences); hasChanges |= compareAttribute("isRecord", local, upstream, differences); hasChanges |= compareAttribute("isSealed", local, upstream, differences); - + // Compare modifiers hasChanges |= compareModifiers(local, upstream, differences); - + // Compare inheritance hasChanges |= compareInheritance(local, upstream, differences); - + // Compare methods hasChanges |= compareMethods(local, upstream, differences); - + // Compare fields hasChanges |= compareFields(local, upstream, differences); - + // Compare constructors hasChanges |= compareConstructors(local, upstream, differences); - + // Set status based on findings if (!hasChanges) { diffMap.put("status", JsonString.of("MATCHING")); @@ -657,15 +647,15 @@ static JsonObject compareApis(JsonObject local, JsonObject upstream) { diffMap.put("status", JsonString.of("DIFFERENT")); diffMap.put("differences", JsonArray.of(differences)); } - + return JsonObject.of(diffMap); } - + /// Compares a simple boolean attribute static boolean compareAttribute(String attrName, JsonObject local, JsonObject upstream, List differences) { final var localValue = local.members().get(attrName); final var upstreamValue = upstream.members().get(attrName); - + if (!Objects.equals(localValue, upstreamValue)) { differences.add(JsonObject.of(Map.of( "type", JsonString.of("attributeChanged"), @@ -677,23 +667,23 @@ static boolean compareAttribute(String attrName, JsonObject local, JsonObject up } return false; } - + /// Compares class modifiers static boolean compareModifiers(JsonObject local, JsonObject upstream, List differences) { final var localMods = (JsonArray) local.members().get("modifiers"); final var upstreamMods = (JsonArray) upstream.members().get("modifiers"); - + if (localMods == null || upstreamMods == null) { return false; } - + final var localSet = localMods.values().stream() .map(v -> ((JsonString) v).value()) .collect(Collectors.toSet()); final var upstreamSet = upstreamMods.values().stream() .map(v -> ((JsonString) v).value()) .collect(Collectors.toSet()); - + if (!localSet.equals(upstreamSet)) { differences.add(JsonObject.of(Map.of( "type", JsonString.of("modifiersChanged"), @@ -704,23 +694,23 @@ static boolean compareModifiers(JsonObject local, JsonObject upstream, List differences) { final var localExtends = (JsonArray) local.members().get("extends"); final var upstreamExtends = (JsonArray) upstream.members().get("extends"); - + if (localExtends == null || upstreamExtends == null) { return false; } - + final var localTypes = localExtends.values().stream() .map(v -> normalizeTypeName(((JsonString) v).value())) .collect(Collectors.toSet()); final var upstreamTypes = upstreamExtends.values().stream() .map(v -> normalizeTypeName(((JsonString) v).value())) .collect(Collectors.toSet()); - + if (!localTypes.equals(upstreamTypes)) { differences.add(JsonObject.of(Map.of( "type", JsonString.of("inheritanceChanged"), @@ -731,18 +721,18 @@ static boolean compareInheritance(JsonObject local, JsonObject upstream, List differences) { final var localMethods = (JsonObject) local.members().get("methods"); final var upstreamMethods = (JsonObject) upstream.members().get("methods"); - + if (localMethods == null || upstreamMethods == null) { return false; } - + var hasChanges = false; - + // Check for removed methods (in local but not upstream) for (final var entry : localMethods.members().entrySet()) { if (!upstreamMethods.members().containsKey(entry.getKey())) { @@ -754,7 +744,7 @@ static boolean compareMethods(JsonObject local, JsonObject upstream, List differences) { final var localFields = (JsonObject) local.members().get("fields"); final var upstreamFields = (JsonObject) upstream.members().get("fields"); - + if (localFields == null || upstreamFields == null) { return false; } - + var hasChanges = false; - + // Check for field differences final var localFieldNames = localFields.members().keySet(); final var upstreamFieldNames = upstreamFields.members().keySet(); - + if (!localFieldNames.equals(upstreamFieldNames)) { differences.add(JsonObject.of(Map.of( "type", JsonString.of("fieldsChanged"), @@ -841,70 +831,70 @@ static boolean compareFields(JsonObject local, JsonObject upstream, List differences) { - final var localCtors = (JsonArray) local.members().get("constructors"); - final var upstreamCtors = (JsonArray) upstream.members().get("constructors"); - - if (localCtors == null || upstreamCtors == null) { + final var localConstructors = (JsonArray) local.members().get("constructors"); + final var upstreamConstructors = (JsonArray) upstream.members().get("constructors"); + + if (localConstructors == null || upstreamConstructors == null) { return false; } - - if (localCtors.values().size() != upstreamCtors.values().size()) { + + if (localConstructors.values().size() != upstreamConstructors.values().size()) { differences.add(JsonObject.of(Map.of( "type", JsonString.of("constructorsChanged"), - "localCount", JsonNumber.of(localCtors.values().size()), - "upstreamCount", JsonNumber.of(upstreamCtors.values().size()) + "localCount", JsonNumber.of(localConstructors.values().size()), + "upstreamCount", JsonNumber.of(upstreamConstructors.values().size()) ))); return true; } - + return false; } - + /// Normalizes type names by removing package prefixes static String normalizeTypeName(String typeName) { // Handle generic types var normalized = typeName; - + // Replace jdk.sandbox.* with standard packages normalized = normalized.replace("jdk.sandbox.java.util.json", "java.util.json"); normalized = normalized.replace("jdk.sandbox.internal.util.json", "jdk.internal.util.json"); - + // Remove any remaining package prefixes for comparison if (normalized.contains(".")) { final var parts = normalized.split("\\."); normalized = parts[parts.length - 1]; } - + return normalized; } - + /// Runs source-to-source comparison for fair parameter name comparison /// @return complete comparison report as JSON static JsonObject runFullComparison() { LOGGER.info("Starting full API comparison"); final var startTime = Instant.now(); - + final var reportMap = new LinkedHashMap(); reportMap.put("timestamp", JsonString.of(startTime.toString())); reportMap.put("localPackage", JsonString.of("jdk.sandbox.java.util.json")); reportMap.put("upstreamPackage", JsonString.of("java.util.json")); - + // Discover local classes final var localClasses = discoverLocalJsonClasses(); LOGGER.info("Found " + localClasses.size() + " local classes"); - + // Extract and compare APIs final var differences = new ArrayList(); var matchingCount = 0; var missingUpstream = 0; var differentApi = 0; - + for (final var clazz : localClasses) { final var className = clazz.getName(); final var localApi = extractLocalApiFromSource(className); @@ -912,7 +902,7 @@ static JsonObject runFullComparison() { final var upstreamApi = extractApiFromSource(upstreamSource, className); final var diff = compareApis(localApi, upstreamApi); differences.add(diff); - + // Count statistics final var status = ((JsonString) diff.members().get("status")).value(); switch (status) { @@ -921,7 +911,7 @@ static JsonObject runFullComparison() { case "DIFFERENT" -> differentApi++; } } - + // Build summary final var summary = JsonObject.of(Map.of( "totalClasses", JsonNumber.of(localClasses.size()), @@ -929,26 +919,26 @@ static JsonObject runFullComparison() { "missingUpstream", JsonNumber.of(missingUpstream), "differentApi", JsonNumber.of(differentApi) )); - + reportMap.put("summary", summary); reportMap.put("differences", JsonArray.of(differences)); - - + + final var duration = Duration.between(startTime, Instant.now()); reportMap.put("durationMs", JsonNumber.of(duration.toMillis())); - + LOGGER.info("Comparison completed in " + duration.toMillis() + "ms"); - + return JsonObject.of(reportMap); } - + /// Fetches single upstream source file static String fetchUpstreamSource(String className) { final var cached = FETCH_CACHE.get(className); if (cached != null) { return cached; } - + final var upstreamPath = mapToUpstreamPath(className); final var url = GITHUB_BASE_URL + upstreamPath; final var source = fetchFromUrl(url); diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java index 111c794..aa140b7 100644 --- a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java @@ -1,65 +1,68 @@ package io.github.simbo1905.tracker; import jdk.sandbox.java.util.json.Json; + import java.util.logging.ConsoleHandler; import java.util.logging.Level; import java.util.logging.Logger; /// Command-line runner for the API Tracker -/// +/// /// Usage: java io.github.simbo1905.tracker.ApiTrackerRunner [loglevel] [mode] [sourcepath] -/// +/// /// Arguments: /// - loglevel: SEVERE, WARNING, INFO, FINE, FINER, FINEST (default: INFO) /// - mode: binary|source (default: binary) /// - binary: Compare binary reflection (local) vs source parsing (remote) /// - source: Compare source parsing (local) vs source parsing (remote) for accurate parameter names /// - sourcepath: Path to local source files (required for source mode) +@SuppressWarnings("JavadocReference") public class ApiTrackerRunner { - - public static void main(String[] args) { - // Parse command line arguments - final var logLevel = args.length > 0 ? Level.parse(args[0].toUpperCase()) : Level.INFO; - final var mode = args.length > 1 ? args[1].toLowerCase() : "binary"; - final var sourcePath = args.length > 2 ? args[2] : null; - - configureLogging(logLevel); - - System.out.println("=== JSON API Tracker ==="); - System.out.println("Comparing local jdk.sandbox.java.util.json with upstream java.util.json"); - System.out.println("Log level: " + logLevel); - System.out.println("Mode: " + mode); - if (sourcePath != null) { - System.out.println("Local source path: " + sourcePath); - } - System.out.println(); - - try { - // Run comparison - now only source-to-source for fair parameter comparison - System.out.println("Running source-to-source comparison for fair parameter names"); - final var report = ApiTracker.runFullComparison(); - - // Pretty print the report - System.out.println("=== Comparison Report ==="); - System.out.println(Json.toDisplayString(report, 2)); - - } catch (Exception e) { - System.err.println("Error during comparison: " + e.getMessage()); - e.printStackTrace(); - System.exit(1); - } + + public static void main(String[] args) { + // Parse command line arguments + final var logLevel = args.length > 0 ? Level.parse(args[0].toUpperCase()) : Level.INFO; + final var mode = args.length > 1 ? args[1].toLowerCase() : "binary"; + final var sourcePath = args.length > 2 ? args[2] : null; + + configureLogging(logLevel); + + System.out.println("=== JSON API Tracker ==="); + System.out.println("Comparing local jdk.sandbox.java.util.json with upstream java.util.json"); + System.out.println("Log level: " + logLevel); + System.out.println("Mode: " + mode); + if (sourcePath != null) { + System.out.println("Local source path: " + sourcePath); + } + System.out.println(); + + try { + // Run comparison - now only source-to-source for fair parameter comparison + System.out.println("Running source-to-source comparison for fair parameter names"); + final var report = ApiTracker.runFullComparison(); + + // Pretty print the report + System.out.println("=== Comparison Report ==="); + System.out.println(Json.toDisplayString(report, 2)); + + } catch (Exception e) { + System.err.println("Error during comparison: " + e.getMessage()); + //noinspection CallToPrintStackTrace + e.printStackTrace(); + System.exit(1); } - - private static void configureLogging(Level level) { - // Get root logger - final var rootLogger = Logger.getLogger(""); - rootLogger.setLevel(level); - - // Configure console handler - for (var handler : rootLogger.getHandlers()) { - if (handler instanceof ConsoleHandler) { - handler.setLevel(level); - } - } + } + + private static void configureLogging(Level level) { + // Get root logger + final var rootLogger = Logger.getLogger(""); + rootLogger.setLevel(level); + + // Configure console handler + for (var handler : rootLogger.getHandlers()) { + if (handler instanceof ConsoleHandler) { + handler.setLevel(level); + } } + } } \ No newline at end of file diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java index 5980d36..c971395 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 @@ -11,33 +11,29 @@ import jdk.sandbox.java.util.json.JsonArray; import jdk.sandbox.java.util.json.JsonObject; import jdk.sandbox.java.util.json.JsonString; -import jdk.sandbox.java.util.json.JsonValue; import java.util.Set; import java.util.Map; -import java.util.logging.Logger; -import java.util.logging.Level; public class ApiTrackerTest { - private static final Logger LOGGER = Logger.getLogger(ApiTrackerTest.class.getName()); - - @BeforeAll + + @BeforeAll static void setupLogging() { LoggingControl.setupCleanLogging(); } - + @Nested @DisplayName("Local Class Discovery") class LocalDiscoveryTests { - + @Test @DisplayName("Should discover JSON API classes") void testDiscoverLocalJsonClasses() { final var classes = ApiTracker.discoverLocalJsonClasses(); - + assertThat(classes).isNotNull(); assertThat(classes).isNotEmpty(); - + // Should find core JSON interfaces assertThat(classes.stream().map(Class::getName)) .contains( @@ -49,28 +45,28 @@ void testDiscoverLocalJsonClasses() { "jdk.sandbox.java.util.json.JsonBoolean", "jdk.sandbox.java.util.json.JsonNull" ); - + // Should NOT find internal implementation classes (public API only) assertThat(classes.stream().anyMatch(c -> c.getName().startsWith("jdk.sandbox.internal.util.json"))) .as("Should not find internal implementation classes - public API only") .isFalse(); - + // Should be sorted final var names = classes.stream().map(Class::getName).toList(); final var sortedNames = names.stream().sorted().toList(); assertThat(names).isEqualTo(sortedNames); } } - + @Nested @DisplayName("Local API Extraction") class LocalApiExtractionTests { - + @Test @DisplayName("Should extract API from JsonObject interface source") void testExtractLocalApiJsonObject() { final var api = ApiTracker.extractLocalApiFromSource("jdk.sandbox.java.util.json.JsonObject"); - + assertThat(api).isNotNull(); // Check if extraction succeeded or failed if (api.members().containsKey("error")) { @@ -81,20 +77,20 @@ void testExtractLocalApiJsonObject() { // If extraction succeeded, validate structure assertThat(api.members()).containsKey("className"); assertThat(((JsonString) api.members().get("className")).value()).isEqualTo("JsonObject"); - + assertThat(api.members()).containsKey("packageName"); assertThat(((JsonString) api.members().get("packageName")).value()).isEqualTo("jdk.sandbox.java.util.json"); - + assertThat(api.members()).containsKey("isInterface"); assertThat(api.members().get("isInterface")).isEqualTo(JsonBoolean.of(true)); } } - + @Test @DisplayName("Should extract API from JsonValue sealed interface source") void testExtractLocalApiJsonValue() { final var api = ApiTracker.extractLocalApiFromSource("jdk.sandbox.java.util.json.JsonValue"); - + // Check if extraction succeeded or failed if (api.members().containsKey("error")) { // If file not found, that's expected for some source setups @@ -104,39 +100,39 @@ void testExtractLocalApiJsonValue() { // If extraction succeeded, validate structure assertThat(api.members()).containsKey("isSealed"); assertThat(api.members().get("isSealed")).isEqualTo(JsonBoolean.of(true)); - + assertThat(api.members()).containsKey("permits"); final var permits = (JsonArray) api.members().get("permits"); // May be empty in source parsing if permits aren't explicitly listed assertThat(permits).isNotNull(); } } - + @Test @DisplayName("Should handle missing source file gracefully") 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(); assertThat(error).contains("LOCAL_FILE_NOT_FOUND"); } } - + @Nested @DisplayName("Upstream Source Fetching") class UpstreamFetchingTests { - + @Test @DisplayName("Should map local class names to upstream paths") void testMapToUpstreamPath() { assertThat(ApiTracker.mapToUpstreamPath("jdk.sandbox.java.util.json.JsonObject")) .isEqualTo("java/util/json/JsonObject.java"); - + assertThat(ApiTracker.mapToUpstreamPath("jdk.sandbox.internal.util.json.JsonObjectImpl")) .isEqualTo("jdk/internal/util/json/JsonObjectImpl.java"); } - + @Test @DisplayName("Should handle null parameter in fetchUpstreamSources") void testFetchUpstreamSourcesNull() { @@ -144,7 +140,7 @@ void testFetchUpstreamSourcesNull() { .isInstanceOf(NullPointerException.class) .hasMessage("localClasses must not be null"); } - + @Test @DisplayName("Should return empty map for empty input") void testFetchUpstreamSourcesEmpty() { @@ -152,25 +148,25 @@ void testFetchUpstreamSourcesEmpty() { assertThat(result).isEmpty(); } } - + @Nested @DisplayName("API Comparison") class ApiComparisonTests { - + @Test @DisplayName("Should handle null parameters in compareApis") void testCompareApisNull() { final var dummyApi = JsonObject.of(Map.of("className", JsonString.of("Test"))); - + assertThatThrownBy(() -> ApiTracker.compareApis(null, dummyApi)) .isInstanceOf(NullPointerException.class) .hasMessage("local must not be null"); - + assertThatThrownBy(() -> ApiTracker.compareApis(dummyApi, null)) .isInstanceOf(NullPointerException.class) .hasMessage("upstream must not be null"); } - + @Test @DisplayName("Should handle upstream errors in comparison") void testCompareApisUpstreamError() { @@ -179,24 +175,24 @@ void testCompareApisUpstreamError() { "error", JsonString.of("NOT_FOUND: File not found"), "className", JsonString.of("TestClass") )); - + final var result = ApiTracker.compareApis(local, upstream); - + assertThat(result.members()).containsKey("status"); assertThat(((JsonString) result.members().get("status")).value()).isEqualTo("UPSTREAM_ERROR"); assertThat(result.members()).containsKey("error"); } } - + @Nested @DisplayName("Full Comparison Orchestration") class FullComparisonTests { - + @Test @DisplayName("Should run full comparison and return report structure") void testRunFullComparison() { final var report = ApiTracker.runFullComparison(); - + assertThat(report).isNotNull(); assertThat(report.members()).containsKeys( "timestamp", @@ -206,7 +202,7 @@ void testRunFullComparison() { "differences", "durationMs" ); - + final var summary = (JsonObject) report.members().get("summary"); assertThat(summary.members()).containsKeys( "totalClasses", @@ -214,26 +210,26 @@ void testRunFullComparison() { "missingUpstream", "differentApi" ); - + // Total classes should be greater than 0 final var totalClasses = summary.members().get("totalClasses"); assertThat(totalClasses).isNotNull(); } } - + @Nested @DisplayName("Type Name Normalization") class TypeNameNormalizationTests { - + @Test @DisplayName("Should normalize type names correctly") void testNormalizeTypeName() { assertThat(ApiTracker.normalizeTypeName("jdk.sandbox.java.util.json.JsonValue")) .isEqualTo("JsonValue"); - + assertThat(ApiTracker.normalizeTypeName("java.lang.String")) .isEqualTo("String"); - + assertThat(ApiTracker.normalizeTypeName("String")) .isEqualTo("String"); } diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java index 626b65f..cdc6658 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java @@ -7,63 +7,64 @@ * for thread-safe lazy initialization. */ class StableValue { - private volatile T value; - private final Object lock = new Object(); + private volatile T value; + private final Object lock = new Object(); - private StableValue() {} + private StableValue() { + } - public static StableValue of() { - return new StableValue<>(); - } + public static StableValue of() { + return new StableValue<>(); + } public T orElse(T defaultValue) { - T result = value; - return result != null ? result : defaultValue; - } + T result = value; + return result != null ? result : defaultValue; + } - public T orElseSet(Supplier supplier) { - T result = value; + public T orElseSet(Supplier supplier) { + T result = value; + if (result == null) { + synchronized (lock) { + result = value; if (result == null) { - synchronized (lock) { - result = value; - if (result == null) { - value = result = supplier.get(); - } - } + value = result = supplier.get(); } - return result; + } } + return result; + } - public void setOrThrow(T newValue) { - if (value != null) { - throw new IllegalStateException("Value already set"); - } - synchronized (lock) { - if (value != null) { - throw new IllegalStateException("Value already set"); - } - value = newValue; - } + public void setOrThrow(T newValue) { + if (value != null) { + throw new IllegalStateException("Value already set"); + } + synchronized (lock) { + if (value != null) { + throw new IllegalStateException("Value already set"); + } + value = newValue; } + } - public static Supplier supplier(Supplier supplier) { - return new Supplier<>() { - private volatile T cached; - private final Object supplierLock = new Object(); + public static Supplier supplier(Supplier supplier) { + return new Supplier<>() { + private volatile T cached; + private final Object supplierLock = new Object(); - @Override - public T get() { - T result = cached; + @Override + public T get() { + T result = cached; + if (result == null) { + synchronized (supplierLock) { + result = cached; if (result == null) { - synchronized (supplierLock) { - result = cached; - if (result == null) { - cached = result = supplier.get(); - } - } + cached = result = supplier.get(); } - return result; } - }; - } + } + return result; + } + }; + } } \ No newline at end of file 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 299d2c0..860358f 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 @@ -12,24 +12,7 @@ public class JsonParserTests { @Test void testParseComplexJson() { - String json = """ - { - "name": "John Doe", - "age": 30, - "isStudent": false, - "courses": [ - {"title": "History", "credits": 3}, - {"title": "Math", "credits": 4} - ], - "address": { - "street": "123 Main St", - "city": "Anytown" - } - } - """; - - JsonParser parser = new JsonParser(json.toCharArray()); - JsonObject jsonObject = (JsonObject) parser.parseRoot(); + JsonObject jsonObject = complexJsonObject(); assertThat(((JsonString) jsonObject.members().get("name")).value()).isEqualTo("John Doe"); assertThat(((JsonNumber) jsonObject.members().get("age")).toNumber().longValue()).isEqualTo(30L); @@ -38,7 +21,7 @@ void testParseComplexJson() { JsonArray courses = (JsonArray) jsonObject.members().get("courses"); assertThat(courses.values()).hasSize(2); - JsonObject course1 = (JsonObject) courses.values().get(0); + 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); @@ -50,4 +33,25 @@ void testParseComplexJson() { assertThat(((JsonString) address.members().get("street")).value()).isEqualTo("123 Main St"); assertThat(((JsonString) address.members().get("city")).value()).isEqualTo("Anytown"); } + + private static JsonObject complexJsonObject() { + String json = """ + { + "name": "John Doe", + "age": 30, + "isStudent": false, + "courses": [ + {"title": "History", "credits": 3}, + {"title": "Math", "credits": 4} + ], + "address": { + "street": "123 Main St", + "city": "Anytown" + } + } + """; + + JsonParser parser = new JsonParser(json.toCharArray()); + return (JsonObject) parser.parseRoot(); + } } 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 c9a5eb9..e7202d1 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 @@ -1,7 +1,6 @@ package jdk.sandbox.internal.util.json; -import jdk.sandbox.java.util.json.Json; import jdk.sandbox.java.util.json.JsonArray; import jdk.sandbox.java.util.json.JsonNumber; import jdk.sandbox.java.util.json.JsonObject; 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 index 6ec623b..a981ce9 100644 --- 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 @@ -12,223 +12,225 @@ 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().get(0)).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; - 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; - - 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"); - } + @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 6764758..79cb926 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 @@ -4,218 +4,221 @@ import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class ReadmeDemoTests { - @Test - void quickStartExample() { - // Basic parsing example - String jsonString = "{\"name\":\"Alice\",\"age\":30}"; - JsonValue value = Json.parse(jsonString); - - 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); - } - - // Domain model using records - record User(String name, String email, boolean active) {} - record Team(String teamName, List members) {} - - @Test - void recordMappingExample() { - // Create a team with users - Team team = new Team("Engineering", List.of( - new User("Alice", "alice@example.com", true), - 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() + @Test + void quickStartExample() { + // Basic parsing example + String jsonString = "{\"name\":\"Alice\",\"age\":30}"; + JsonValue value = Json.parse(jsonString); + + 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); + } + + // Domain model using records + record User(String name, String email, boolean active) { + } + + record Team(String teamName, List members) { + } + + @Test + void recordMappingExample() { + // Create a team with users + Team team = new Team("Engineering", List.of( + new User("Alice", "alice@example.com", true), + 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() + )); + + // Verify the JSON structure + assertThat(teamJson).isInstanceOf(JsonObject.class); + JsonObject teamObj = (JsonObject) teamJson; + assertThat(((JsonString) teamObj.members().get("teamName")).value()).isEqualTo("Engineering"); + + JsonArray members = (JsonArray) teamObj.members().get("members"); + assertThat(members.values()).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() + .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() + ); + + // Verify reconstruction + assertThat(reconstructed).isEqualTo(team); + assertThat(reconstructed.teamName()).isEqualTo("Engineering"); + assertThat(reconstructed.members()).hasSize(2); + assertThat(reconstructed.members().get(0).name()).isEqualTo("Alice"); + assertThat(reconstructed.members().get(0).active()).isTrue(); + assertThat(reconstructed.members().get(1).name()).isEqualTo("Bob"); + assertThat(reconstructed.members().get(1).active()).isFalse(); + } + + @Test + void builderPatternExample() { + // Building a 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") )) - .toList() - )); - - // Verify the JSON structure - assertThat(teamJson).isInstanceOf(JsonObject.class); - JsonObject teamObj = (JsonObject) teamJson; - assertThat(((JsonString) teamObj.members().get("teamName")).value()).isEqualTo("Engineering"); - - JsonArray members = (JsonArray) teamObj.members().get("members"); - assertThat(members.values()).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() - .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() - ); - - // Verify reconstruction - assertThat(reconstructed).isEqualTo(team); - assertThat(reconstructed.teamName()).isEqualTo("Engineering"); - assertThat(reconstructed.members()).hasSize(2); - assertThat(reconstructed.members().get(0).name()).isEqualTo("Alice"); - assertThat(reconstructed.members().get(0).active()).isTrue(); - assertThat(reconstructed.members().get(1).name()).isEqualTo("Bob"); - assertThat(reconstructed.members().get(1).active()).isFalse(); - } - - @Test - void builderPatternExample() { - // Building a 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(1234567890L) )), - "errors", JsonArray.of(List.of()) - )); - - // Verify structure - assertThat(((JsonString) response.members().get("status")).value()).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"); - - JsonArray roles = (JsonArray) user.members().get("roles"); - assertThat(roles.values()).hasSize(2); - assertThat(((JsonString) roles.values().get(0)).value()).isEqualTo("admin"); - - JsonArray errors = (JsonArray) response.members().get("errors"); - assertThat(errors.values()).isEmpty(); - } - - @Test - void streamingProcessingExample() { - // Create a large array of user records - String largeJsonArray = """ - [ - {"name": "Alice", "email": "alice@example.com", "active": true}, - {"name": "Bob", "email": "bob@example.com", "active": false}, - {"name": "Charlie", "email": "charlie@example.com", "active": true}, - {"name": "David", "email": "david@example.com", "active": false}, - {"name": "Eve", "email": "eve@example.com", "active": true} - ] - """; - - // Process a large array of records - JsonArray items = (JsonArray) Json.parse(largeJsonArray); - List activeUserEmails = items.values().stream() - .map(v -> (JsonObject) v) - .filter(obj -> ((JsonBoolean) obj.members().get("active")).value()) - .map(obj -> ((JsonString) obj.members().get("email")).value()) - .toList(); - - // Verify we got only active users - assertThat(activeUserEmails).containsExactly( - "alice@example.com", - "charlie@example.com", - "eve@example.com" - ); - } - - @Test - void errorHandlingExample() { - // Valid JSON parsing - String validJson = "{\"valid\": true}"; - JsonValue value = Json.parse(validJson); - assertThat(value).isInstanceOf(JsonObject.class); - - // Invalid JSON parsing - String invalidJson = "{invalid json}"; - assertThatThrownBy(() -> Json.parse(invalidJson)) - .isInstanceOf(JsonParseException.class) - .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 - @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 - JsonObject data = JsonObject.of(Map.of( - "name", JsonString.of("Alice"), - "scores", JsonArray.of(List.of( - JsonNumber.of(85), - JsonNumber.of(90), - JsonNumber.of(95) - )) - )); - - // Format for display - String formatted = Json.toDisplayString(data, 2); - - // Verify it contains proper formatting (checking key parts) - assertThat(formatted).contains("{\n"); - assertThat(formatted).contains(" \"name\": \"Alice\""); - assertThat(formatted).contains(" \"scores\": ["); - assertThat(formatted).contains(" 85,"); - assertThat(formatted).contains(" 90,"); - assertThat(formatted).contains(" 95"); - assertThat(formatted).contains(" ]"); - assertThat(formatted).contains("}"); - } + "timestamp", JsonNumber.of(1234567890L) + )), + "errors", JsonArray.of(List.of()) + )); + + // Verify structure + assertThat(((JsonString) response.members().get("status")).value()).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"); + + JsonArray roles = (JsonArray) user.members().get("roles"); + assertThat(roles.values()).hasSize(2); + assertThat(((JsonString) roles.values().getFirst()).value()).isEqualTo("admin"); + + JsonArray errors = (JsonArray) response.members().get("errors"); + assertThat(errors.values()).isEmpty(); + } + + @Test + void streamingProcessingExample() { + // Create a large array of user records + String largeJsonArray = """ + [ + {"name": "Alice", "email": "alice@example.com", "active": true}, + {"name": "Bob", "email": "bob@example.com", "active": false}, + {"name": "Charlie", "email": "charlie@example.com", "active": true}, + {"name": "David", "email": "david@example.com", "active": false}, + {"name": "Eve", "email": "eve@example.com", "active": true} + ] + """; + + // Process a large array of records + JsonArray items = (JsonArray) Json.parse(largeJsonArray); + List activeUserEmails = items.values().stream() + .map(v -> (JsonObject) v) + .filter(obj -> ((JsonBoolean) obj.members().get("active")).value()) + .map(obj -> ((JsonString) obj.members().get("email")).value()) + .toList(); + + // Verify we got only active users + assertThat(activeUserEmails).containsExactly( + "alice@example.com", + "charlie@example.com", + "eve@example.com" + ); + } + + @Test + void errorHandlingExample() { + // Valid JSON parsing + String validJson = "{\"valid\": true}"; + JsonValue value = Json.parse(validJson); + assertThat(value).isInstanceOf(JsonObject.class); + + // Invalid JSON parsing + String invalidJson = "{invalid json}"; + assertThatThrownBy(() -> Json.parse(invalidJson)) + .isInstanceOf(JsonParseException.class) + .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 + JsonObject data = JsonObject.of(Map.of( + "name", JsonString.of("Alice"), + "scores", JsonArray.of(List.of( + JsonNumber.of(85), + JsonNumber.of(90), + JsonNumber.of(95) + )) + )); + + // Format for display + String formatted = Json.toDisplayString(data, 2); + + // Verify it contains proper formatting (checking key parts) + assertThat(formatted).contains("{\n"); + assertThat(formatted).contains(" \"name\": \"Alice\""); + assertThat(formatted).contains(" \"scores\": ["); + assertThat(formatted).contains(" 85,"); + assertThat(formatted).contains(" 90,"); + assertThat(formatted).contains(" 95"); + assertThat(formatted).contains(" ]"); + assertThat(formatted).contains("}"); + } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 086aeb5..4909985 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,8 @@ 3.1.2 + + @@ -95,12 +97,6 @@ org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} - - - -Xlint:all - -Werror - - org.apache.maven.plugins