Skip to content

Commit 1a7f5df

Browse files
committed
move to source parsing for local api comparison
1 parent c399c90 commit 1a7f5df

File tree

3 files changed

+179
-22
lines changed

3 files changed

+179
-22
lines changed

json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,85 @@
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
6569
public 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

json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,54 @@
77

88
/// Command-line runner for the API Tracker
99
///
10-
/// Usage: java io.github.simbo1905.tracker.ApiTrackerRunner [loglevel]
11-
/// where loglevel is one of: SEVERE, WARNING, INFO, FINE, FINER, FINEST
10+
/// Usage: java io.github.simbo1905.tracker.ApiTrackerRunner [loglevel] [mode] [sourcepath]
11+
///
12+
/// Arguments:
13+
/// - loglevel: SEVERE, WARNING, INFO, FINE, FINER, FINEST (default: INFO)
14+
/// - mode: binary|source (default: binary)
15+
/// - binary: Compare binary reflection (local) vs source parsing (remote)
16+
/// - source: Compare source parsing (local) vs source parsing (remote) for accurate parameter names
17+
/// - sourcepath: Path to local source files (required for source mode)
1218
public class ApiTrackerRunner {
1319

1420
public static void main(String[] args) {
15-
// Configure logging based on command line argument
21+
// Parse command line arguments
1622
final var logLevel = args.length > 0 ? Level.parse(args[0].toUpperCase()) : Level.INFO;
23+
final var mode = args.length > 1 ? args[1].toLowerCase() : "binary";
24+
final var sourcePath = args.length > 2 ? args[2] : null;
25+
1726
configureLogging(logLevel);
1827

1928
System.out.println("=== JSON API Tracker ===");
2029
System.out.println("Comparing local jdk.sandbox.java.util.json with upstream java.util.json");
2130
System.out.println("Log level: " + logLevel);
31+
System.out.println("Mode: " + mode);
32+
if (sourcePath != null) {
33+
System.out.println("Local source path: " + sourcePath);
34+
}
2235
System.out.println();
2336

2437
try {
25-
// Run the full comparison
26-
final var report = ApiTracker.runFullComparison();
38+
// Run comparison based on mode
39+
final var report = switch (mode) {
40+
case "binary" -> {
41+
System.out.println("Running binary reflection vs source parsing comparison");
42+
yield ApiTracker.runFullComparison();
43+
}
44+
case "source" -> {
45+
if (sourcePath == null) {
46+
System.err.println("Error: source mode requires sourcepath argument");
47+
System.exit(1);
48+
}
49+
System.out.println("Running source-to-source comparison (apples-to-apples)");
50+
yield ApiTracker.runSourceToSourceComparison(sourcePath);
51+
}
52+
default -> {
53+
System.err.println("Error: mode must be 'binary' or 'source'");
54+
System.exit(1);
55+
yield null; // Never reached
56+
}
57+
};
2758

2859
// Pretty print the report
2960
System.out.println("=== Comparison Report ===");

json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class LocalApiExtractionTests {
7070
@DisplayName("Should extract API from JsonObject interface")
7171
void testExtractLocalApiJsonObject() throws ClassNotFoundException {
7272
final var clazz = Class.forName("jdk.sandbox.java.util.json.JsonObject");
73-
final var api = ApiTracker.extractLocalApi(clazz);
73+
final var api = ApiTracker.extractLocalApiFromClass(clazz);
7474

7575
assertThat(api).isNotNull();
7676
assertThat(api.members()).containsKey("className");
@@ -91,7 +91,7 @@ void testExtractLocalApiJsonObject() throws ClassNotFoundException {
9191
@DisplayName("Should extract API from JsonValue sealed interface")
9292
void testExtractLocalApiJsonValue() throws ClassNotFoundException {
9393
final var clazz = Class.forName("jdk.sandbox.java.util.json.JsonValue");
94-
final var api = ApiTracker.extractLocalApi(clazz);
94+
final var api = ApiTracker.extractLocalApiFromClass(clazz);
9595

9696
assertThat(api.members()).containsKey("isSealed");
9797
assertThat(api.members().get("isSealed")).isEqualTo(JsonBoolean.of(true));
@@ -104,7 +104,7 @@ void testExtractLocalApiJsonValue() throws ClassNotFoundException {
104104
@Test
105105
@DisplayName("Should handle null class parameter")
106106
void testExtractLocalApiNull() {
107-
assertThatThrownBy(() -> ApiTracker.extractLocalApi(null))
107+
assertThatThrownBy(() -> ApiTracker.extractLocalApiFromClass(null))
108108
.isInstanceOf(NullPointerException.class)
109109
.hasMessage("clazz must not be null");
110110
}

0 commit comments

Comments
 (0)