6161/// - Compare public APIs using compiler parsing
6262/// - Generate structured diff reports
6363///
64+ /// Modular design supports different extraction strategies:
65+ /// - Binary reflection for quick class introspection
66+ /// - Source parsing for accurate parameter names and signatures
67+ ///
6468/// All functionality is exposed as static methods following functional programming principles
6569public sealed interface ApiTracker permits ApiTracker .Nothing {
6670
71+ /// API extraction strategy interface
72+ sealed interface ApiExtractor permits
73+ ReflectionApiExtractor , SourceApiExtractor {
74+ JsonObject extractApi (String identifier );
75+ }
76+
77+ /// Source location strategy interface
78+ sealed interface SourceLocator permits
79+ LocalSourceLocator , RemoteSourceLocator {
80+ String locateSource (String className );
81+ }
82+
83+ /// Reflection-based API extractor
84+ record ReflectionApiExtractor () implements ApiExtractor {
85+ @ Override
86+ public JsonObject extractApi (String className ) {
87+ try {
88+ final var clazz = Class .forName (className );
89+ return extractLocalApiFromClass (clazz );
90+ } catch (ClassNotFoundException e ) {
91+ return JsonObject .of (Map .of (
92+ "error" , JsonString .of ("CLASS_NOT_FOUND: " + e .getMessage ()),
93+ "className" , JsonString .of (className )
94+ ));
95+ }
96+ }
97+ }
98+
99+ /// Source-based API extractor
100+ record SourceApiExtractor (SourceLocator sourceLocator ) implements ApiExtractor {
101+ @ Override
102+ public JsonObject extractApi (String className ) {
103+ final var sourceCode = sourceLocator .locateSource (className );
104+ return extractApiFromSource (sourceCode , className );
105+ }
106+ }
107+
108+ /// Local source file locator
109+ record LocalSourceLocator (String sourceRoot ) implements SourceLocator {
110+ @ Override
111+ public String locateSource (String className ) {
112+ final var path = sourceRoot + "/" + className .replace ('.' , '/' ) + ".java" ;
113+ try {
114+ return java .nio .file .Files .readString (java .nio .file .Paths .get (path ));
115+ } catch (Exception e ) {
116+ return "FILE_NOT_FOUND: " + e .getMessage ();
117+ }
118+ }
119+ }
120+
121+ /// Remote source locator (GitHub)
122+ record RemoteSourceLocator (String baseUrl ) implements SourceLocator {
123+ @ Override
124+ public String locateSource (String className ) {
125+ final var upstreamPath = mapToUpstreamPath (className );
126+ final var url = baseUrl + upstreamPath ;
127+ return fetchFromUrl (url );
128+ }
129+ }
130+
131+ /// Comparison orchestrator
132+ record ApiComparator (
133+ ApiExtractor localExtractor ,
134+ ApiExtractor upstreamExtractor
135+ ) {
136+ JsonObject compare (String className ) {
137+ final var localApi = localExtractor .extractApi (className );
138+ final var upstreamApi = upstreamExtractor .extractApi (className );
139+ return compareApis (localApi , upstreamApi );
140+ }
141+ }
142+
67143 /// Empty enum to seal the interface - no instances allowed
68144 enum Nothing implements ApiTracker {}
69145
@@ -76,6 +152,33 @@ enum Nothing implements ApiTracker {}
76152 // GitHub base URL for upstream sources
77153 static final String GITHUB_BASE_URL = "https://raw.githubusercontent.com/openjdk/jdk-sandbox/refs/heads/json/src/java.base/share/classes/" ;
78154
155+ /// Fetches content from a URL
156+ static String fetchFromUrl (String url ) {
157+ final var httpClient = HttpClient .newBuilder ()
158+ .connectTimeout (Duration .ofSeconds (10 ))
159+ .build ();
160+
161+ try {
162+ final var request = HttpRequest .newBuilder ()
163+ .uri (URI .create (url ))
164+ .timeout (Duration .ofSeconds (30 ))
165+ .GET ()
166+ .build ();
167+
168+ final var response = httpClient .send (request , HttpResponse .BodyHandlers .ofString ());
169+
170+ if (response .statusCode () == 200 ) {
171+ return response .body ();
172+ } else if (response .statusCode () == 404 ) {
173+ return "NOT_FOUND: Upstream file not found (possibly deleted or renamed)" ;
174+ } else {
175+ return "HTTP_ERROR: Status " + response .statusCode ();
176+ }
177+ } catch (Exception e ) {
178+ return "FETCH_ERROR: " + e .getMessage ();
179+ }
180+ }
181+
79182 /// Discovers all classes in the local JSON API packages
80183 /// @return sorted set of classes from jdk.sandbox.java.util.json and jdk.sandbox.internal.util.json
81184 static Set <Class <?>> discoverLocalJsonClasses () {
@@ -261,7 +364,7 @@ static String mapToUpstreamPath(String className) {
261364 /// Extracts public API from a compiled class using reflection
262365 /// @param clazz the class to extract API from
263366 /// @return JSON representation of the class's public API
264- static JsonObject extractLocalApi (Class <?> clazz ) {
367+ static JsonObject extractLocalApiFromClass (Class <?> clazz ) {
265368 Objects .requireNonNull (clazz , "clazz must not be null" );
266369 LOGGER .info ("Extracting local API for: " + clazz .getName ());
267370
@@ -397,11 +500,11 @@ static JsonArray extractConstructors(Class<?> clazz) {
397500 return JsonArray .of (constructors );
398501 }
399502
400- /// Extracts public API from upstream source code using compiler parsing
503+ /// Extracts public API from source code using compiler parsing
401504 /// @param sourceCode the source code to parse
402505 /// @param className the expected class name
403506 /// @return JSON representation of the parsed API
404- static JsonObject extractUpstreamApi (String sourceCode , String className ) {
507+ static JsonObject extractApiFromSource (String sourceCode , String className ) {
405508 Objects .requireNonNull (sourceCode , "sourceCode must not be null" );
406509 Objects .requireNonNull (className , "className must not be null" );
407510
@@ -701,7 +804,11 @@ static JsonObject compareApis(JsonObject local, JsonObject upstream) {
701804 Objects .requireNonNull (upstream , "upstream must not be null" );
702805
703806 final var diffMap = new LinkedHashMap <String , JsonValue >();
704- final var className = ((JsonString ) local .members ().get ("className" )).value ();
807+
808+ // Extract class name safely
809+ final var localClassName = local .members ().get ("className" );
810+ final var className = localClassName instanceof JsonString js ?
811+ js .value () : "Unknown" ;
705812
706813 diffMap .put ("className" , JsonString .of (className ));
707814
@@ -980,9 +1087,33 @@ static String normalizeTypeName(String typeName) {
9801087 return normalized ;
9811088 }
9821089
983- /// Runs a full comparison of local vs upstream APIs
984- /// @return complete comparison report as JSON
1090+ /// Runs a full comparison of local vs upstream APIs using reflection vs source parsing
1091+ /// @return complete comparison report as JSON
9851092 static JsonObject runFullComparison () {
1093+ return runComparisonWithExtractors (
1094+ new ReflectionApiExtractor (),
1095+ new SourceApiExtractor (new RemoteSourceLocator (GITHUB_BASE_URL ))
1096+ );
1097+ }
1098+
1099+ /// Runs a source-to-source comparison for apples-to-apples comparison
1100+ /// @param localSourceRoot path to local source files
1101+ /// @return complete comparison report as JSON
1102+ static JsonObject runSourceToSourceComparison (String localSourceRoot ) {
1103+ return runComparisonWithExtractors (
1104+ new SourceApiExtractor (new LocalSourceLocator (localSourceRoot )),
1105+ new SourceApiExtractor (new RemoteSourceLocator (GITHUB_BASE_URL ))
1106+ );
1107+ }
1108+
1109+ /// Runs comparison with specified extractors
1110+ /// @param localExtractor extractor for local API
1111+ /// @param upstreamExtractor extractor for upstream API
1112+ /// @return complete comparison report as JSON
1113+ static JsonObject runComparisonWithExtractors (
1114+ ApiExtractor localExtractor ,
1115+ ApiExtractor upstreamExtractor
1116+ ) {
9861117 LOGGER .info ("Starting full API comparison" );
9871118 final var startTime = Instant .now ();
9881119
@@ -995,21 +1126,16 @@ static JsonObject runFullComparison() {
9951126 final var localClasses = discoverLocalJsonClasses ();
9961127 LOGGER .info ("Found " + localClasses .size () + " local classes" );
9971128
998- // Fetch upstream sources
999- final var upstreamSources = fetchUpstreamSources (localClasses );
1000-
10011129 // Extract and compare APIs
10021130 final var differences = new ArrayList <JsonValue >();
10031131 var matchingCount = 0 ;
10041132 var missingUpstream = 0 ;
10051133 var differentApi = 0 ;
10061134
1135+ final var comparator = new ApiComparator (localExtractor , upstreamExtractor );
1136+
10071137 for (final var clazz : localClasses ) {
1008- final var localApi = extractLocalApi (clazz );
1009- final var upstreamSource = upstreamSources .get (clazz .getName ());
1010- final var upstreamApi = extractUpstreamApi (upstreamSource , clazz .getName ());
1011-
1012- final var diff = compareApis (localApi , upstreamApi );
1138+ final var diff = comparator .compare (clazz .getName ());
10131139 differences .add (diff );
10141140
10151141 // Count statistics
0 commit comments