diff --git a/.github/workflows/daily-api-tracker.yml b/.github/workflows/daily-api-tracker.yml new file mode 100644 index 0000000..5541a76 --- /dev/null +++ b/.github/workflows/daily-api-tracker.yml @@ -0,0 +1,59 @@ +name: Daily API Tracker + +on: + schedule: + # Run daily at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: # Allow manual trigger for testing + +jobs: + track-api: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 24 + uses: actions/setup-java@v4 + with: + java-version: '24' + distribution: 'temurin' + + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Build project + run: mvn clean compile -DskipTests + + - name: Run API Tracker + run: | + mvn exec:java \ + -pl json-java21-api-tracker \ + -Dexec.mainClass="io.github.simbo1905.tracker.ApiTrackerRunner" \ + -Dexec.args="INFO" \ + -Djava.util.logging.ConsoleHandler.level=INFO + + - name: Create issue if differences found + if: failure() + uses: actions/github-script@v7 + with: + script: | + const title = 'API differences detected between local and upstream'; + const body = `The daily API tracker found differences between our local implementation and upstream. + + Check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. + + Date: ${new Date().toISOString().split('T')[0]}`; + + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: ['api-tracking', 'upstream-sync'] + }); \ No newline at end of file diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 671b5ba..e7d3ea5 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -10,18 +10,15 @@ on: jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - java: [ '21', '22', '23', '24' ] - name: Build with JDK ${{ matrix.java }} + name: Build with JDK 24 steps: - uses: actions/checkout@v4 - - name: Set up JDK ${{ matrix.java }} + - name: Set up JDK 24 uses: actions/setup-java@v4 with: - java-version: ${{ matrix.java }} + java-version: '24' distribution: 'oracle' - name: Cache Maven dependencies diff --git a/.gitignore b/.gitignore index 667c04f..4658f71 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ target/ .idea/ .claude/ +.aider* +CLAUDE.md diff --git a/CODING_STYLE_LLM.md b/CODING_STYLE_LLM.md new file mode 100644 index 0000000..48853b3 --- /dev/null +++ b/CODING_STYLE_LLM.md @@ -0,0 +1,133 @@ +# Java DOP Coding Standards #################### + +This file is a Gen AI summary of CODING_STYLE.md to use less tokens of context window. Read the original file for full details. + +IMPORTANT: We do TDD so all code must include targeted unit tests. +IMPORTANT: Never disable tests written for logic that we are yet to write we do Red-Green-Refactor coding. + +## Core Principles + +* Use Records for all data structures. Use sealed interfaces for protocols. +* Prefer static methods with Records as parameters +* Default to package-private scope +* Package-by-feature, not package-by-layer +* Create fewer, cohesive, wide packages (functionality modules or records as protocols) +* Use public only when cross-package access is required +* Use JEP 467 Markdown documentation examples: `/// good markdown` not legacy `/** bad html */` +* Apply Data-Oriented Programming principles and avoid OOP +* Use Stream operations instead of traditional loops. Never use `for(;;)` with mutable loop variables use + `Arrays.setAll` +* Prefer exhaustive destructuring switch expressions over if-else statements +* Use destructuring switch expressions that operate on Records and sealed interfaces +* Use anonymous variables in record destructuring and switch expressions +* Use `final var` for local variables, parameters, and destructured fields +* Apply JEP 371 "Local Classes and Interfaces" for cohesive files with narrow APIs + +## Data-Oriented Programming + +* Separate data (immutable Records) from behavior (never utility classes always static methods) +* Use immutable generic data structures (maps, lists, sets) and take defense copies in constructors +* Write pure functions that don't modify state +* Leverage Java 21+ features: + * Records for immutable data + * Pattern matching for structural decomposition + * Sealed classes for exhaustive switches + * Virtual threads for concurrent processing + +## Package Structure + +* Use default (package-private) access as the standard. Do not use 'private' or 'public' by default. +* Limit public to genuine cross-package APIs +* Prefer package-private static methods. Do not use 'private' or 'public' by default. +* Limit private to security-related code +* Avoid anti-patterns: boilerplate OOP, excessive layering, dependency injection overuse + +## Constants and Magic Numbers + +* **NEVER use magic numbers** - always use enum constants +* **NEVER write large if-else-if statements over known types** - will not be exhaustive and creates bugs when new types are added. Use exhaustive switch statements over bounded sets such as enum values or sealed interface permits + +## Functional Style + +* Combine Records + static methods for functional programming +* Emphasize immutability and explicit state transformations +* Reduce package count to improve testability +* Implement Algebraic Data Types pattern with Function Modules +* Modern Stream Programming +* Use Stream API instead of traditional loops +* Write declarative rather than imperative code +* Chain operations without intermediate variables +* Support immutability throughout processing +* Example: `IntStream.range(0, 100).filter(i -> i % 2 == 0).sum()` instead of counting loops +* Always use final variables in functional style. +* Prefer `final var` with self documenting names over `int i` or `String s` but its not possible to do that on a `final` variable that is not yet initialized so its a weak preference not a strong one. +* Avoid just adding new functionality to the top of a method to make an early return. It is fine to have a simple guard statement. Yet general you should pattern match over the input to do different things with the same method. Adding special case logic is a code smell that should be avoided. + +## Documentation using JEP 467 Markdown documentation + +IMPORTANT: You must not write JavaDoc comments that start with `/**` and end with `*/` +IMPORTANT: You must "JEP 467: Markdown Documentation Comments" that start all lines with `///` + +Here is an example of the correct format for documentation comments: + +```java +/// Returns a hash code value for the object. This method is +/// supported for the benefit of hash tables such as those provided by +/// [java.util.HashMap]. +/// +/// The general contract of `hashCode` is: +/// +/// - Whenever it is invoked on the same object more than once during +/// an execution of a Java application, the `hashCode` method +/// - If two objects are equal according to the +/// [equals][#equals(Object)] method, then calling the +/// - It is _not_ required that if two objects are unequal +/// according to the [equals][#equals(Object)] method, then +/// +/// @return a hash code value for this object. +/// @see java.lang.Object#equals(java.lang.Object) +``` + +## Logging + +- Use Java's built-in logging: `java.util.logging.Logger` +- Log levels: Use appropriate levels (FINE, FINER, INFO, WARNING, SEVERE) + - **FINE**: Production-level debugging, default for most debug output + - **FINER**: Verbose debugging, detailed internal flow, class resolution details + - **INFO**: Important runtime information +- LOGGER is a static field: `static final Logger LOGGER = Logger.getLogger(ClassName.class.getName());` where use the primary interface or the package as the logger name with the logger package-private and shared across the classes when the package is small enough. +- Use lambda logging for performance: `LOGGER.fine(() -> "message " + variable);` + +# Compile, Test, Debug Loop + +- **Check Compiles**: Focusing on the correct mvn module run without verbose logging and do not grep the output to see compile errors: + ```bash + ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Djava.util.logging.ConsoleHandler.level=SEVERE + ``` +- **Debug with Verbose Logs**: Use `-Dtest=` to focus on just one or two test methods, or one class, using more logging to debug the code: + ```bash + ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Dtest=XXX -Djava.util.logging.ConsoleHandler.level=FINER + ``` +- **No Grep Filtering**: Use logging levels to filter output, do not grep the output for compile errors, just run less test methods with the correct logging to reduce the output to a manageable size. Filtering hides problems and needs more test excution to find the same problems which wastes time. + +## Modern Java Singleton Pattern: Sealed Interfaces + +**Singleton Object Anti-Pattern**: Traditional singleton classes with private constructors and static instances are legacy should be avoided. With a functional style we can create a "package-private companion module" of small package-private methods with `sealed interfacee GoodSingletonModule permits Nothing { enum Nothing extends GoodSingletonModule{}; /* static functional methods here */ }`. + +### Assertions and Input Validation + +1. On the public API entry points use `Objects.assertNonNull()` to ensure that the inputs are legal. +2. After that on internal method that should be passed only valid data use `assert` to ensure that the data is valid. + - e.g. use `assert x==y: "unexpected x="+x+" y="+y;` as `mvn` base should be run with `-ea` to enable assertions. +3. Often there is an `orElseThrow()` which can be used so the only reason to use `assert` is to add more logging to the error message. +4. Consider using the validations of `Object` and `Arrays` and the like to ensure that the data is valid. + - e.g. `Objects.requireNonNull(type, "type must not be null")` or `Arrays.checkIndex(index, array.length)`. + +## JEP References + +[JEP 467](https://openjdk.org/jeps/467): Markdown Documentation in JavaDoc +[JEP 371](https://openjdk.org/jeps/371): Local Classes and Interfaces +[JEP 395](https://openjdk.org/jeps/395): Records +[JEP 409](https://openjdk.org/jeps/409): Sealed Classes +[JEP 440](https://openjdk.org/jeps/440): Record Patterns +[JEP 427](https://openjdk.org/jeps/427): Pattern Matching for Switch diff --git a/json-java21-api-tracker/pom.xml b/json-java21-api-tracker/pom.xml new file mode 100644 index 0000000..eb03e82 --- /dev/null +++ b/json-java21-api-tracker/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + + io.github.simbo1905.json + json-java21-parent + 0.1-SNAPSHOT + + + json-java21-api-tracker + jar + + API Tracker + + + 24 + + + + + io.github.simbo1905.json + json-java21 + ${project.version} + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + + + 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 + + --enable-preview + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000..5e13d92 --- /dev/null +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java @@ -0,0 +1,1042 @@ +package io.github.simbo1905.tracker; + +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 jdk.sandbox.java.util.json.JsonNumber; +import jdk.sandbox.java.util.json.JsonBoolean; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +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.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ModifiersTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.TreePathScanner; +import com.sun.source.util.Trees; + +/// 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 +/// - Compare public APIs using compiler parsing +/// - Generate structured diff reports +/// +/// All functionality is exposed as static methods following functional programming principles +public sealed interface ApiTracker permits ApiTracker.Nothing { + + /// 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()); + + // Cache for HTTP responses to avoid repeated fetches + static final 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/"; + + /// 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())); + + // 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); + } else if ("jar".equals(url.getProtocol())) { + // Handle JAR scanning + scanJar(url, packageName, classes); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error scanning package: " + packageName, e); + } + } + + 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 + '.' + + file.getName().substring(0, file.getName().length() - 6); + try { + final var clazz = Class.forName(className); + classes.add(clazz); + LOGGER.info("Found class: " + className); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + LOGGER.fine(() -> "Could not load class: " + className); + } + } + } + } + + /// Scans a JAR file for classes in the specified package + static void scanJar(java.net.URL jarUrl, String packageName, Set> classes) { + try { + final var jarPath = jarUrl.getPath(); + final var exclamation = jarPath.indexOf('!'); + 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") && + !entryName.contains("$")) { + + final var className = entryName + .substring(0, entryName.length() - 6) + .replace('/', '.'); + + try { + final var clazz = Class.forName(className); + classes.add(clazz); + LOGGER.info("Found class in JAR: " + className); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + LOGGER.fine(() -> "Could not load class from JAR: " + className); + } + } + } + } + } catch (Exception e) { + 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); + results.put(className, body); + LOGGER.info("Successfully fetched " + body.length() + " chars for: " + className); + } else if (response.statusCode() == 404) { + final var error = "NOT_FOUND: Upstream file not found (possibly deleted or renamed)"; + results.put(className, error); + LOGGER.info("404 Not Found for upstream: " + className + " at " + url); + } else { + final var error = "HTTP_ERROR: Status " + response.statusCode(); + results.put(className, error); + LOGGER.info("HTTP error " + response.statusCode() + " for " + className + " at " + url); + } + } catch (Exception e) { + final var error = "FETCH_ERROR: " + e.getMessage(); + results.put(className, error); + 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 + String path = 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 public API from a compiled class using reflection + /// @param clazz the class to extract API from + /// @return JSON representation of the class's public API + static JsonObject extractLocalApi(Class clazz) { + Objects.requireNonNull(clazz, "clazz must not be null"); + LOGGER.info("Extracting local API for: " + clazz.getName()); + + final var apiMap = new LinkedHashMap(); + + // Basic class information + apiMap.put("className", JsonString.of(clazz.getSimpleName())); + apiMap.put("packageName", JsonString.of(clazz.getPackage() != null ? clazz.getPackage().getName() : "")); + apiMap.put("modifiers", extractModifiers(clazz.getModifiers())); + + // Type information + apiMap.put("isInterface", JsonBoolean.of(clazz.isInterface())); + apiMap.put("isEnum", JsonBoolean.of(clazz.isEnum())); + apiMap.put("isRecord", JsonBoolean.of(clazz.isRecord())); + apiMap.put("isSealed", JsonBoolean.of(clazz.isSealed())); + + // Inheritance + final var superTypes = new ArrayList(); + if (clazz.getSuperclass() != null && !Object.class.equals(clazz.getSuperclass())) { + superTypes.add(JsonString.of(clazz.getSuperclass().getSimpleName())); + } + Arrays.stream(clazz.getInterfaces()) + .map(i -> JsonString.of(i.getSimpleName())) + .forEach(superTypes::add); + apiMap.put("extends", JsonArray.of(superTypes)); + + // Permitted subclasses (for sealed types) + if (clazz.isSealed()) { + final var permits = Arrays.stream(clazz.getPermittedSubclasses()) + .map(c -> JsonString.of(c.getSimpleName())) + .collect(Collectors.toList()); + apiMap.put("permits", JsonArray.of(permits)); + } + + // Methods + apiMap.put("methods", extractMethods(clazz)); + + // Fields + apiMap.put("fields", extractFields(clazz)); + + // Constructors + apiMap.put("constructors", extractConstructors(clazz)); + + return JsonObject.of(apiMap); + } + + /// Extracts modifiers as JSON array + static JsonArray extractModifiers(int modifiers) { + final var modList = new ArrayList(); + + if (Modifier.isPublic(modifiers)) modList.add(JsonString.of("public")); + if (Modifier.isProtected(modifiers)) modList.add(JsonString.of("protected")); + if (Modifier.isPrivate(modifiers)) modList.add(JsonString.of("private")); + if (Modifier.isStatic(modifiers)) modList.add(JsonString.of("static")); + if (Modifier.isFinal(modifiers)) modList.add(JsonString.of("final")); + if (Modifier.isAbstract(modifiers)) modList.add(JsonString.of("abstract")); + if (Modifier.isNative(modifiers)) modList.add(JsonString.of("native")); + if (Modifier.isSynchronized(modifiers)) modList.add(JsonString.of("synchronized")); + if (Modifier.isTransient(modifiers)) modList.add(JsonString.of("transient")); + if (Modifier.isVolatile(modifiers)) modList.add(JsonString.of("volatile")); + + return JsonArray.of(modList); + } + + /// Extracts public methods + static JsonObject extractMethods(Class clazz) { + final var methodsMap = new LinkedHashMap(); + + Arrays.stream(clazz.getDeclaredMethods()) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .forEach(method -> { + final var methodInfo = new LinkedHashMap(); + methodInfo.put("modifiers", extractModifiers(method.getModifiers())); + methodInfo.put("returnType", JsonString.of(method.getReturnType().getSimpleName())); + methodInfo.put("genericReturnType", JsonString.of(method.getGenericReturnType().getTypeName())); + + final var params = Arrays.stream(method.getParameters()) + .map(p -> JsonString.of(p.getType().getSimpleName() + " " + p.getName())) + .collect(Collectors.toList()); + methodInfo.put("parameters", JsonArray.of(params)); + + final var exceptions = Arrays.stream(method.getExceptionTypes()) + .map(e -> JsonString.of(e.getSimpleName())) + .collect(Collectors.toList()); + methodInfo.put("throws", JsonArray.of(exceptions)); + + methodsMap.put(method.getName(), JsonObject.of(methodInfo)); + }); + + return JsonObject.of(methodsMap); + } + + /// Extracts public fields + static JsonObject extractFields(Class clazz) { + final var fieldsMap = new LinkedHashMap(); + + Arrays.stream(clazz.getDeclaredFields()) + .filter(f -> Modifier.isPublic(f.getModifiers())) + .forEach(field -> { + final var fieldInfo = new LinkedHashMap(); + fieldInfo.put("modifiers", extractModifiers(field.getModifiers())); + fieldInfo.put("type", JsonString.of(field.getType().getSimpleName())); + fieldInfo.put("genericType", JsonString.of(field.getGenericType().getTypeName())); + + fieldsMap.put(field.getName(), JsonObject.of(fieldInfo)); + }); + + return JsonObject.of(fieldsMap); + } + + /// Extracts public constructors + static JsonArray extractConstructors(Class clazz) { + final var constructors = Arrays.stream(clazz.getDeclaredConstructors()) + .filter(c -> Modifier.isPublic(c.getModifiers())) + .map(constructor -> { + final var ctorInfo = new LinkedHashMap(); + ctorInfo.put("modifiers", extractModifiers(constructor.getModifiers())); + + final var params = Arrays.stream(constructor.getParameters()) + .map(p -> JsonString.of(p.getType().getSimpleName() + " " + p.getName())) + .collect(Collectors.toList()); + ctorInfo.put("parameters", JsonArray.of(params)); + + final var exceptions = Arrays.stream(constructor.getExceptionTypes()) + .map(e -> JsonString.of(e.getSimpleName())) + .collect(Collectors.toList()); + ctorInfo.put("throws", JsonArray.of(exceptions)); + + return JsonObject.of(ctorInfo); + }) + .collect(Collectors.toList()); + + return JsonArray.of(constructors); + } + + /// Extracts public API from upstream source code using compiler parsing + /// @param sourceCode the source code to parse + /// @param className the expected class name + /// @return JSON representation of the parsed API + static JsonObject extractUpstreamApi(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:") || + sourceCode.startsWith("FETCH_ERROR:")) { + final var errorMap = Map.of( + "error", JsonString.of(sourceCode), + "className", JsonString.of(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( + "error", JsonString.of("JavaCompiler not available"), + "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", + "-XDignore.symbol.file", + "-Xlint:none", + "--enable-preview", + "--release", "24" + ); + + final var task = (JavacTask) compiler.getTask( + null, + fileManager, + diagnostics, + options, + null, + compilationUnits + ); + + final var trees = task.parse(); + + // Extract API using visitor + for (final var tree : trees) { + final var fileName = tree.getSourceFile().getName(); + if (fileName.contains(simpleClassName)) { + final var visitor = new ApiExtractorVisitor(); + visitor.scan(tree, null); + 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( + "error", JsonString.of("Parse error: " + e.getMessage()), + "className", JsonString.of(className) + )); + } finally { + try { + fileManager.close(); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Error closing file manager", e); + } + } + } + + /// Adds common stub dependencies for JSON API parsing + static void addCommonStubs(List compilationUnits) { + // PreviewFeature annotation stub + compilationUnits.add(new InMemoryJavaFileObject("jdk.internal.javac.PreviewFeature", """ + package jdk.internal.javac; + import java.lang.annotation.*; + @Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface PreviewFeature { + Feature feature(); + 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; + import java.util.Map; + import java.util.json.JsonObject; + import java.util.json.JsonValue; + public class JsonObjectImpl implements JsonObject { + public JsonObjectImpl(Map map) {} + public Map members() { return null; } + public boolean equals(Object obj) { return false; } + public int hashCode() { return 0; } + } + """)); + } + + /// In-memory JavaFileObject for creating stub classes + static class InMemoryJavaFileObject extends SimpleJavaFileObject { + private final String content; + + InMemoryJavaFileObject(String className, String content) { + super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.content = content; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return content; + } + } + + /// Visitor to extract API information from AST + static class ApiExtractorVisitor extends TreePathScanner { + private final Map apiMap = new LinkedHashMap<>(); + private final Map methodsMap = new LinkedHashMap<>(); + private final Map fieldsMap = new LinkedHashMap<>(); + private final List constructors = new ArrayList<>(); + + JsonObject getExtractedApi() { + apiMap.put("methods", JsonObject.of(methodsMap)); + apiMap.put("fields", JsonObject.of(fieldsMap)); + apiMap.put("constructors", JsonArray.of(constructors)); + return JsonObject.of(apiMap); + } + + @Override + 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(); + if (packageTree != null) { + apiMap.put("packageName", JsonString.of(packageTree.getPackageName().toString())); + } 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) { + superTypes.add(JsonString.of(extractSimpleName(node.getExtendsClause().toString()))); + } + node.getImplementsClause().stream() + .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); + } + + @Override + 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())); + methodInfo.put("returnType", JsonString.of(extractSimpleName( + 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)); + } else { + methodsMap.put(node.getName().toString(), JsonObject.of(methodInfo)); + } + } + + return super.visitMethod(node, p); + } + + @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(); + if (parent != null && parent.getLeaf().getKind() == Tree.Kind.INTERFACE) { + return !modifiers.getFlags().contains(javax.lang.model.element.Modifier.PRIVATE) && + !modifiers.getFlags().contains(javax.lang.model.element.Modifier.DEFAULT); + } + return modifiers.getFlags().contains(javax.lang.model.element.Modifier.PUBLIC); + } + + private String extractSimpleName(String typeName) { + // Remove generic parameters and package prefixes + var name = typeName; + final var genericIndex = name.indexOf('<'); + if (genericIndex >= 0) { + name = name.substring(0, genericIndex); + } + final var lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + name = name.substring(lastDot + 1); + } + return name; + } + } + + /// Compares local and upstream APIs to identify differences + /// @param local the local API structure + /// @param upstream the upstream API structure + /// @return JSON object describing the differences + 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(); + final var className = ((JsonString) local.members().get("className")).value(); + + 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(); + if ("NOT_IMPLEMENTED".equals(status)) { + diffMap.put("status", JsonString.of("PARSE_NOT_IMPLEMENTED")); + 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")); + } else { + 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"), + "attribute", JsonString.of(attrName), + "local", localValue != null ? localValue : JsonBoolean.of(false), + "upstream", upstreamValue != null ? upstreamValue : JsonBoolean.of(false) + ))); + return true; + } + 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"), + "local", localMods, + "upstream", upstreamMods + ))); + return true; + } + return false; + } + + /// Compares inheritance hierarchy + static boolean compareInheritance(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"), + "local", localExtends, + "upstream", upstreamExtends + ))); + return true; + } + return false; + } + + /// Compares methods between local and upstream + static boolean compareMethods(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())) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("methodRemoved"), + "method", JsonString.of(entry.getKey()), + "details", entry.getValue() + ))); + hasChanges = true; + } + } + + // Check for added methods (in upstream but not local) + for (final var entry : upstreamMethods.members().entrySet()) { + if (!localMethods.members().containsKey(entry.getKey())) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("methodAdded"), + "method", JsonString.of(entry.getKey()), + "details", entry.getValue() + ))); + hasChanges = true; + } + } + + // Check for changed methods + for (final var entry : localMethods.members().entrySet()) { + final var methodName = entry.getKey(); + if (upstreamMethods.members().containsKey(methodName)) { + final var localMethod = (JsonObject) entry.getValue(); + final var upstreamMethod = (JsonObject) upstreamMethods.members().get(methodName); + + if (!compareMethodSignature(localMethod, upstreamMethod)) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("methodChanged"), + "method", JsonString.of(methodName), + "local", localMethod, + "upstream", upstreamMethod + ))); + hasChanges = true; + } + } + } + + return hasChanges; + } + + /// Compares method signatures + static boolean compareMethodSignature(JsonObject localMethod, JsonObject upstreamMethod) { + // Compare return types + final var localReturn = normalizeTypeName(((JsonString) localMethod.members().get("returnType")).value()); + final var upstreamReturn = normalizeTypeName(((JsonString) upstreamMethod.members().get("returnType")).value()); + if (!localReturn.equals(upstreamReturn)) { + return false; + } + + // Compare parameters + final var localParams = (JsonArray) localMethod.members().get("parameters"); + final var upstreamParams = (JsonArray) upstreamMethod.members().get("parameters"); + + if (localParams.values().size() != upstreamParams.values().size()) { + return false; + } + + // Compare each parameter + for (int i = 0; i < localParams.values().size(); i++) { + final var localParam = normalizeTypeName(((JsonString) localParams.values().get(i)).value()); + final var upstreamParam = normalizeTypeName(((JsonString) upstreamParams.values().get(i)).value()); + if (!localParam.equals(upstreamParam)) { + return false; + } + } + + return true; + } + + /// Compares fields between local and upstream + static boolean compareFields(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"), + "local", JsonArray.of(localFieldNames.stream().map(JsonString::of).collect(Collectors.toList())), + "upstream", JsonArray.of(upstreamFieldNames.stream().map(JsonString::of).collect(Collectors.toList())) + ))); + hasChanges = true; + } + + return hasChanges; + } + + /// Compares constructors between local and upstream + static boolean compareConstructors(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) { + return false; + } + + if (localCtors.values().size() != upstreamCtors.values().size()) { + differences.add(JsonObject.of(Map.of( + "type", JsonString.of("constructorsChanged"), + "localCount", JsonNumber.of(localCtors.values().size()), + "upstreamCount", JsonNumber.of(upstreamCtors.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 a full comparison of local vs upstream APIs + /// @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"); + + // Fetch upstream sources + final var upstreamSources = fetchUpstreamSources(localClasses); + + // 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 localApi = extractLocalApi(clazz); + final var upstreamSource = upstreamSources.get(clazz.getName()); + final var upstreamApi = extractUpstreamApi(upstreamSource, clazz.getName()); + + final var diff = compareApis(localApi, upstreamApi); + differences.add(diff); + + // Count statistics + final var status = ((JsonString) diff.members().get("status")).value(); + switch (status) { + case "MATCHING" -> matchingCount++; + case "UPSTREAM_ERROR" -> missingUpstream++; + case "DIFFERENT" -> differentApi++; + } + } + + // Build summary + final var summary = JsonObject.of(Map.of( + "totalClasses", JsonNumber.of(localClasses.size()), + "matchingClasses", JsonNumber.of(matchingCount), + "missingUpstream", JsonNumber.of(missingUpstream), + "differentApi", JsonNumber.of(differentApi) + )); + + reportMap.put("summary", summary); + reportMap.put("differences", JsonArray.of(differences)); + + final var duration = Duration.between(startTime, Instant.now()); + reportMap.put("durationMs", JsonNumber.of(duration.toMillis())); + + LOGGER.info("Comparison completed in " + duration.toMillis() + "ms"); + + return JsonObject.of(reportMap); + } +} \ No newline at end of file diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java new file mode 100644 index 0000000..49f1427 --- /dev/null +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTrackerRunner.java @@ -0,0 +1,51 @@ +package io.github.simbo1905.tracker; + +import jdk.sandbox.java.util.json.Json; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/// Command-line runner for the API Tracker +/// +/// Usage: java io.github.simbo1905.tracker.ApiTrackerRunner [loglevel] +/// where loglevel is one of: SEVERE, WARNING, INFO, FINE, FINER, FINEST +public class ApiTrackerRunner { + + public static void main(String[] args) { + // Configure logging based on command line argument + final var logLevel = args.length > 0 ? Level.parse(args[0].toUpperCase()) : Level.INFO; + configureLogging(logLevel); + + System.out.println("=== JSON API Tracker ==="); + System.out.println("Comparing local jdk.sandbox.java.util.json with upstream java.util.json"); + System.out.println("Log level: " + logLevel); + System.out.println(); + + try { + // Run the full comparison + final var report = ApiTracker.runFullComparison(); + + // Pretty print the report + System.out.println("=== Comparison Report ==="); + System.out.println(Json.toDisplayString(report, 2)); + + } catch (Exception e) { + System.err.println("Error during comparison: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + private static void configureLogging(Level level) { + // Get root logger + final var rootLogger = Logger.getLogger(""); + rootLogger.setLevel(level); + + // Configure console handler + for (var handler : rootLogger.getHandlers()) { + if (handler instanceof ConsoleHandler) { + handler.setLevel(level); + } + } + } +} \ No newline at end of file diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java new file mode 100644 index 0000000..0a2d853 --- /dev/null +++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java @@ -0,0 +1,230 @@ +package io.github.simbo1905.tracker; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jdk.sandbox.java.util.json.JsonBoolean; +import jdk.sandbox.java.util.json.JsonArray; +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonString; +import jdk.sandbox.java.util.json.JsonValue; + +import java.util.Set; +import java.util.Map; +import java.util.logging.Logger; +import java.util.logging.Level; + +public class ApiTrackerTest { + private static final Logger LOGGER = Logger.getLogger(ApiTrackerTest.class.getName()); + + @BeforeAll + static void setupLogging() { + LoggingControl.setupCleanLogging(); + } + + @Nested + @DisplayName("Local Class Discovery") + class LocalDiscoveryTests { + + @Test + @DisplayName("Should discover JSON API classes") + void testDiscoverLocalJsonClasses() { + final var classes = ApiTracker.discoverLocalJsonClasses(); + + assertThat(classes).isNotNull(); + assertThat(classes).isNotEmpty(); + + // Should find core JSON interfaces + assertThat(classes.stream().map(Class::getName)) + .contains( + "jdk.sandbox.java.util.json.JsonValue", + "jdk.sandbox.java.util.json.JsonObject", + "jdk.sandbox.java.util.json.JsonArray", + "jdk.sandbox.java.util.json.JsonString", + "jdk.sandbox.java.util.json.JsonNumber", + "jdk.sandbox.java.util.json.JsonBoolean", + "jdk.sandbox.java.util.json.JsonNull" + ); + + // Should NOT find internal implementation classes (public API only) + assertThat(classes.stream().anyMatch(c -> c.getName().startsWith("jdk.sandbox.internal.util.json"))) + .as("Should not find internal implementation classes - public API only") + .isFalse(); + + // Should be sorted + final var names = classes.stream().map(Class::getName).toList(); + final var sortedNames = names.stream().sorted().toList(); + assertThat(names).isEqualTo(sortedNames); + } + } + + @Nested + @DisplayName("Local API Extraction") + class LocalApiExtractionTests { + + @Test + @DisplayName("Should extract API from JsonObject interface") + void testExtractLocalApiJsonObject() throws ClassNotFoundException { + final var clazz = Class.forName("jdk.sandbox.java.util.json.JsonObject"); + final var api = ApiTracker.extractLocalApi(clazz); + + assertThat(api).isNotNull(); + assertThat(api.members()).containsKey("className"); + assertThat(((JsonString) api.members().get("className")).value()).isEqualTo("JsonObject"); + + assertThat(api.members()).containsKey("packageName"); + assertThat(((JsonString) api.members().get("packageName")).value()).isEqualTo("jdk.sandbox.java.util.json"); + + assertThat(api.members()).containsKey("isInterface"); + assertThat(api.members().get("isInterface")).isEqualTo(JsonBoolean.of(true)); + + assertThat(api.members()).containsKey("methods"); + final var methods = (JsonObject) api.members().get("methods"); + assertThat(methods.members()).containsKeys("members", "of"); + } + + @Test + @DisplayName("Should extract API from JsonValue sealed interface") + void testExtractLocalApiJsonValue() throws ClassNotFoundException { + final var clazz = Class.forName("jdk.sandbox.java.util.json.JsonValue"); + final var api = ApiTracker.extractLocalApi(clazz); + + assertThat(api.members()).containsKey("isSealed"); + assertThat(api.members().get("isSealed")).isEqualTo(JsonBoolean.of(true)); + + assertThat(api.members()).containsKey("permits"); + final var permits = (JsonArray) api.members().get("permits"); + assertThat(permits.values()).isNotEmpty(); + } + + @Test + @DisplayName("Should handle null class parameter") + void testExtractLocalApiNull() { + assertThatThrownBy(() -> ApiTracker.extractLocalApi(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("clazz must not be null"); + } + } + + @Nested + @DisplayName("Upstream Source Fetching") + class UpstreamFetchingTests { + + @Test + @DisplayName("Should map local class names to upstream paths") + void testMapToUpstreamPath() { + assertThat(ApiTracker.mapToUpstreamPath("jdk.sandbox.java.util.json.JsonObject")) + .isEqualTo("java/util/json/JsonObject.java"); + + assertThat(ApiTracker.mapToUpstreamPath("jdk.sandbox.internal.util.json.JsonObjectImpl")) + .isEqualTo("jdk/internal/util/json/JsonObjectImpl.java"); + } + + @Test + @DisplayName("Should handle null parameter in fetchUpstreamSources") + void testFetchUpstreamSourcesNull() { + assertThatThrownBy(() -> ApiTracker.fetchUpstreamSources(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("localClasses must not be null"); + } + + @Test + @DisplayName("Should return empty map for empty input") + void testFetchUpstreamSourcesEmpty() { + final var result = ApiTracker.fetchUpstreamSources(Set.of()); + assertThat(result).isEmpty(); + } + } + + @Nested + @DisplayName("API Comparison") + class ApiComparisonTests { + + @Test + @DisplayName("Should handle null parameters in compareApis") + void testCompareApisNull() { + final var dummyApi = JsonObject.of(Map.of("className", JsonString.of("Test"))); + + assertThatThrownBy(() -> ApiTracker.compareApis(null, dummyApi)) + .isInstanceOf(NullPointerException.class) + .hasMessage("local must not be null"); + + assertThatThrownBy(() -> ApiTracker.compareApis(dummyApi, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("upstream must not be null"); + } + + @Test + @DisplayName("Should handle upstream errors in comparison") + void testCompareApisUpstreamError() { + final var local = JsonObject.of(Map.of("className", JsonString.of("TestClass"))); + final var upstream = JsonObject.of(Map.of( + "error", JsonString.of("NOT_FOUND: File not found"), + "className", JsonString.of("TestClass") + )); + + final var result = ApiTracker.compareApis(local, upstream); + + assertThat(result.members()).containsKey("status"); + assertThat(((JsonString) result.members().get("status")).value()).isEqualTo("UPSTREAM_ERROR"); + assertThat(result.members()).containsKey("error"); + } + } + + @Nested + @DisplayName("Full Comparison Orchestration") + class FullComparisonTests { + + @Test + @DisplayName("Should run full comparison and return report structure") + void testRunFullComparison() { + final var report = ApiTracker.runFullComparison(); + + assertThat(report).isNotNull(); + assertThat(report.members()).containsKeys( + "timestamp", + "localPackage", + "upstreamPackage", + "summary", + "differences", + "durationMs" + ); + + final var summary = (JsonObject) report.members().get("summary"); + assertThat(summary.members()).containsKeys( + "totalClasses", + "matchingClasses", + "missingUpstream", + "differentApi" + ); + + // Total classes should be greater than 0 + final var totalClasses = summary.members().get("totalClasses"); + assertThat(totalClasses).isNotNull(); + } + } + + @Nested + @DisplayName("Modifier Extraction") + class ModifierExtractionTests { + + @Test + @DisplayName("Should extract modifiers correctly") + void testExtractModifiers() { + // Test public static final + final var modifiers = java.lang.reflect.Modifier.PUBLIC | + java.lang.reflect.Modifier.STATIC | + java.lang.reflect.Modifier.FINAL; + + final var result = ApiTracker.extractModifiers(modifiers); + + assertThat(result.values()).hasSize(3); + assertThat(result.values().stream().map(v -> ((JsonString) v).value())) + .containsExactlyInAnyOrder("public", "static", "final"); + } + } +} \ No newline at end of file diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java new file mode 100644 index 0000000..d54522a --- /dev/null +++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/LoggingControl.java @@ -0,0 +1,52 @@ +package io.github.simbo1905.tracker; + +import java.util.logging.*; + +/// Modern Java companion object pattern for test logging configuration. +/// Uses sealed interface with config record to avoid singleton anti-patterns. +/// Provides clean, compact output instead of JUL's ugly two-line default format. +public sealed interface LoggingControl permits LoggingControl.Config { + + /// Configuration record for logging setup + record Config(Level defaultLevel) implements LoggingControl { + } + + /// Set up clean, compact logging format for tests using functional style. + /// No instances, no singletons - just clean configuration via records and default methods. + static void setupCleanLogging(Config config) { + // Allow CLI override via -Djava.util.logging.ConsoleHandler.level=FINER + String logLevel = System.getProperty("java.util.logging.ConsoleHandler.level"); + Level level = (logLevel != null) ? Level.parse(logLevel) : config.defaultLevel(); + + // Get the root logger to configure globally + Logger rootLogger = Logger.getLogger(""); + + // Remove default handlers to prevent ugly JUL formatting + for (Handler handler : rootLogger.getHandlers()) { + rootLogger.removeHandler(handler); + } + + // Create console handler with clean formatting + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setLevel(level); + + // Custom formatter for compact single-line output (saves tokens and money) + consoleHandler.setFormatter(new Formatter() { + @Override + public String format(LogRecord record) { + return record.getMessage() + "\n"; + } + }); + + rootLogger.addHandler(consoleHandler); + rootLogger.setLevel(level); + } + + /// Convenience method with default WARNING level + static void setupCleanLogging() { + Level level = System.getProperty("java.util.logging.ConsoleHandler.level") != null + ? Level.parse(System.getProperty("java.util.logging.ConsoleHandler.level")) + : Level.WARNING; + setupCleanLogging(new Config(level)); + } +} \ No newline at end of file diff --git a/json-java21-api-tracker/src/test/resources/JsonObject.java b/json-java21-api-tracker/src/test/resources/JsonObject.java new file mode 100644 index 0000000..ad98b40 --- /dev/null +++ b/json-java21-api-tracker/src/test/resources/JsonObject.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.util.json; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import jdk.internal.javac.PreviewFeature; +import jdk.internal.util.json.JsonObjectImpl; + +/** + * The interface that represents JSON object. + * + *

+ * A {@code JsonObject} can be produced by a {@link Json#parse(String)}. + * + * Alternatively, {@link #of(Map)} can be used to obtain a {@code JsonObject}. + * Implementations of {@code JsonObject} cannot be created from sources that + * contain duplicate member names. If duplicate names appear during + * a {@link Json#parse(String)}, a {@code JsonParseException} is thrown. + * + * @since 99 + */ +@PreviewFeature(feature = PreviewFeature.Feature.JSON) +public non-sealed interface JsonObject extends JsonValue { + + /** + * {@return an unmodifiable map of the {@code String} to {@code JsonValue} + * members in this {@code JsonObject}} + */ + Map members(); + + /** + * {@return the {@code JsonObject} created from the given + * map of {@code String} to {@code JsonValue}s} + * + * The {@code JsonObject}'s members occur in the same order as the given + * map's entries. + * + * @param map the map of {@code JsonValue}s. Non-null. + * @throws NullPointerException if {@code map} is {@code null}, contains + * any keys that are {@code null}, or contains any values that are {@code null}. + */ + static JsonObject of(Map map) { + return new JsonObjectImpl(map.entrySet() // Implicit NPE on map + .stream() + .collect(Collectors.toMap( + e -> Objects.requireNonNull(e.getKey()), + Map.Entry::getValue, // Implicit NPE on val + (_, v) -> v, + LinkedHashMap::new))); + } + + /** + * {@return {@code true} if the given object is also a {@code JsonObject} + * and the two {@code JsonObject}s represent the same mappings} Two + * {@code JsonObject}s {@code jo1} and {@code jo2} represent the same + * mappings if {@code jo1.members().equals(jo2.members())}. + * + * @see #members() + */ + @Override + boolean equals(Object obj); + + /** + * {@return the hash code value for this {@code JsonObject}} The hash code value + * of a {@code JsonObject} is derived from the hash code of {@code JsonObject}'s + * {@link #members()}. Thus, for two {@code JsonObject}s {@code jo1} and {@code jo2}, + * {@code jo1.equals(jo2)} implies that {@code jo1.hashCode() == jo2.hashCode()} + * as required by the general contract of {@link Object#hashCode}. + * + * @see #members() + */ + @Override + int hashCode(); +} diff --git a/json-java21/pom.xml b/json-java21/pom.xml new file mode 100644 index 0000000..10ade61 --- /dev/null +++ b/json-java21/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + io.github.simbo1905.json + json-java21-parent + 0.1-SNAPSHOT + + + json-java21 + jar + + java.util.json Backport + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + diff --git a/src/main/java/jdk/sandbox/demo/JsonDemo.java b/json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java similarity index 100% rename from src/main/java/jdk/sandbox/demo/JsonDemo.java rename to json-java21/src/main/java/jdk/sandbox/demo/JsonDemo.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonParser.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/StableValue.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/StableValue.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/StableValue.java diff --git a/src/main/java/jdk/sandbox/internal/util/json/Utils.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java similarity index 100% rename from src/main/java/jdk/sandbox/internal/util/json/Utils.java rename to json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java diff --git a/src/main/java/jdk/sandbox/java/util/json/Json.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/Json.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonArray.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonArray.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonNull.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonNull.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNull.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonNumber.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonObject.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonObject.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonParseException.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonString.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonString.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonValue.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/JsonValue.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java diff --git a/src/main/java/jdk/sandbox/java/util/json/package-info.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java similarity index 100% rename from src/main/java/jdk/sandbox/java/util/json/package-info.java rename to json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java diff --git a/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java similarity index 100% rename from src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java rename to json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java diff --git a/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java similarity index 100% rename from src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java rename to json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java diff --git a/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java similarity index 100% rename from src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java rename to json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java diff --git a/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java similarity index 100% rename from src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java rename to json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java diff --git a/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java similarity index 100% rename from src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java rename to json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java diff --git a/mvn-test-no-boilerplate.sh b/mvn-test-no-boilerplate.sh new file mode 100755 index 0000000..4142448 --- /dev/null +++ b/mvn-test-no-boilerplate.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Strip Maven test boilerplate - show compile errors and test results only +# Usage: ./mvn-test-no-boilerplate.sh [maven test arguments] +# +# Examples: +# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests +# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=INFO +# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=FINER +# +# For running tests in a specific module: +# ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Dtest=CompilerApiLearningTest +# +# The script automatically detects if mvnd is available, otherwise falls back to mvn + +# Detect if mvnd is available, otherwise use mvn +if command -v mvnd &> /dev/null; then + MVN_CMD="mvnd" +else + MVN_CMD="mvn" +fi + +$MVN_CMD test "$@" 2>&1 | awk ' +BEGIN { + scanning_started = 0 + compilation_section = 0 + test_section = 0 +} + +# Skip all WARNING lines before project scanning starts +/INFO.*Scanning for projects/ { + scanning_started = 1 + print + next +} + +# Before scanning starts, skip WARNING lines +!scanning_started && /^WARNING:/ { next } + +# Show compilation errors +/COMPILATION ERROR/ { compilation_section = 1 } +/BUILD FAILURE/ && compilation_section { compilation_section = 0 } + +# Show test section +/INFO.*T E S T S/ { + test_section = 1 + print "-------------------------------------------------------" + print " T E S T S" + print "-------------------------------------------------------" + next +} + +# In compilation error section, show everything +compilation_section { print } + +# In test section, show everything - let user control logging with -D arguments +test_section { + print +} + +# Before test section starts, show important lines only +!test_section && scanning_started { + if (/INFO.*Scanning|INFO.*Building|INFO.*resources|INFO.*compiler|INFO.*surefire|ERROR|FAILURE/) { + print + } + # Show compilation warnings/errors + if (/WARNING.*COMPILATION|ERROR.*/) { + print + } +} +' \ No newline at end of file diff --git a/pom.xml b/pom.xml index 03aca4c..086aeb5 100644 --- a/pom.xml +++ b/pom.xml @@ -1,80 +1,123 @@ - - 4.0.0 + + + 4.0.0 - jdk-sandbox - json-experimental - 0.1-SNAPSHOT - jar + io.github.simbo1905.json + json-java21-parent + 0.1-SNAPSHOT + pom - java.util.json Backport for JDK 21+ - Early access to future java.util.json API - tracking OpenJDK sandbox development - https://simbo1905.github.io/java.util.json.Java21/ + java.util.json Backport Parent + A backport of the upcoming java.util.json API for Java 21+ + https://simbo1905.github.io/java.util.json.Java21/ - - - GNU General Public License, version 2, with the Classpath Exception - https://www.gnu.org/licenses/old-licenses/gpl-2.0.html - repo - - + + + GNU General Public License, version 2, with the Classpath Exception + https://openjdk.org/legal/gplv2+ce.html + + - - 21 - 21 - 5.13.1 - 1.13.1 - + + + Simon + simon@simon.com + simon + https://github.com/simbo1905 + + - - - org.junit.jupiter - junit-jupiter-api - ${junit.jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.jupiter.version} - test - - - org.junit.platform - junit-platform-launcher - ${junit.platform.version} - test - - - org.junit.platform - junit-platform-console - ${junit.platform.version} - test - - - org.assertj - assertj-core - 3.26.3 - test - - + + scm:git:git://github.com/simbo1905/java.util.json.Java21.git + scm:git:ssh://github.com:simbo1905/java.util.json.Java21.git + https://github.com/simbo1905/java.util.json.Java21/tree/main + - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 21 - 21 - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.2.5 - - - - + + json-java21 + json-java21-api-tracker + + + + UTF-8 + 21 + 5.10.2 + 3.25.3 + + + 3.4.0 + 3.3.1 + 3.13.0 + 3.2.5 + 3.4.2 + 3.1.2 + + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + -Xlint:all + -Werror + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + org.apache.maven.plugins + maven-install-plugin + ${maven-install-plugin.version} + + + + + \ No newline at end of file