Init Commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
# Reflection Config Generator
|
||||||
|
|
||||||
|
Ein kleines CLI-Tool, das Java-Quellcode scannt und daraus eine
|
||||||
|
`reflection-config.json` fuer Quarkus/GraalVM erzeugt.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- PowerShell 5.1+ oder PowerShell 7+
|
||||||
|
- Optional: Python 3.9+ (wenn du die Python-Variante nutzen willst)
|
||||||
|
|
||||||
|
## Nutzung
|
||||||
|
|
||||||
|
### PowerShell (empfohlen)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\reflection_config_generator.ps1 `
|
||||||
|
-SourceRoot C:\apps\service-a\src\main\java `
|
||||||
|
-Package com.example `
|
||||||
|
-Output C:\apps\service-a\src\main\resources\META-INF\native-image\reflection-config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Erzeugt standardmaessig `reflection-config.json` im aktuellen Verzeichnis, wenn `-Output` nicht gesetzt ist.
|
||||||
|
|
||||||
|
### Mehrere Source-Roots und Packages
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\reflection_config_generator.ps1 `
|
||||||
|
-SourceRoot C:\apps\service-a\src\main\java, C:\apps\service-b\src\main\java `
|
||||||
|
-Package com.company.project, org.shared.*
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ausgabe auf stdout
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\reflection_config_generator.ps1 `
|
||||||
|
-SourceRoot C:\apps\service-a\src\main\java `
|
||||||
|
-Package com.example `
|
||||||
|
-Stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Optionen
|
||||||
|
|
||||||
|
- `-SourceRoot`: Quellverzeichnis, rekursiv gescannt (mehrfach/als Liste nutzbar)
|
||||||
|
- `-Package`: Package-Praefix(e), z. B. `com.acme` oder `com.acme.*`
|
||||||
|
- `-Output`: Zielpfad (Standard: `reflection-config.json`)
|
||||||
|
- `-Stdout`: JSON statt Datei direkt in die Konsole
|
||||||
|
- `-IncludeDeclared:$true/$false`: Declared-Member-Flags (Standard: true)
|
||||||
|
- `-IncludePublic:$true/$false`: Public-Member-Flags (Standard: false)
|
||||||
|
- `-IncludeConstructors:$true/$false`
|
||||||
|
- `-IncludeMethods:$true/$false`
|
||||||
|
- `-IncludeFields:$true/$false`
|
||||||
|
- `-NoSort`: Discovery-Reihenfolge behalten
|
||||||
|
|
||||||
|
## Python-Variante (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python reflection_config_generator.py --package com.example
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
|
||||||
|
- Das Tool liest `.java`-Dateien (nicht `.class`-Dateien).
|
||||||
|
- Es werden Top-Level-Typen (`class`, `interface`, `enum`, `record`) erfasst.
|
||||||
|
- Dateien ohne `package`-Deklaration werden ignoriert.
|
||||||
Binary file not shown.
@@ -0,0 +1,464 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter()]
|
||||||
|
[string[]]$SourceRoot = @("."),
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string[]]$Package = @(),
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$Output = "reflection-config.json",
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$Stdout,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$IncludeDeclared = $true,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$IncludePublic = $false,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$IncludeConstructors = $true,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$IncludeMethods = $true,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$IncludeFields = $true,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$NoSort
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Normalize-PackageFilter {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$RawFilter
|
||||||
|
)
|
||||||
|
|
||||||
|
$normalized = $RawFilter.Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($normalized)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalized.EndsWith(".*", [System.StringComparison]::Ordinal)) {
|
||||||
|
$normalized = $normalized.Substring(0, $normalized.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized.TrimEnd(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Parse-PackageFilters {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$Values
|
||||||
|
)
|
||||||
|
|
||||||
|
$filters = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||||
|
|
||||||
|
foreach ($value in $Values) {
|
||||||
|
foreach ($part in $value.Split(",")) {
|
||||||
|
$normalized = Normalize-PackageFilter -RawFilter $part
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($normalized) -and $seen.Add($normalized)) {
|
||||||
|
$filters.Add($normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filters.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-PackageMatch {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PackageName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$PackageFilters
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($PackageFilters.Count -eq 0) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($prefix in $PackageFilters) {
|
||||||
|
if ($PackageName.Equals($prefix, [System.StringComparison]::Ordinal)) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PackageName.StartsWith("$prefix.", [System.StringComparison]::Ordinal)) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Strip-CommentsAndLiterals {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Source
|
||||||
|
)
|
||||||
|
|
||||||
|
$result = [System.Text.StringBuilder]::new($Source.Length)
|
||||||
|
$state = "normal"
|
||||||
|
$length = $Source.Length
|
||||||
|
$i = 0
|
||||||
|
|
||||||
|
while ($i -lt $length) {
|
||||||
|
$char = $Source[$i]
|
||||||
|
$next = if ($i + 1 -lt $length) { $Source[$i + 1] } else { [char]0 }
|
||||||
|
|
||||||
|
if ($state -eq "normal") {
|
||||||
|
if ($char -eq "/" -and $next -eq "/") {
|
||||||
|
$state = "line_comment"
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
$i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char -eq "/" -and $next -eq "*") {
|
||||||
|
$state = "block_comment"
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
$i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char -eq '"') {
|
||||||
|
$state = "string"
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
$i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char -eq "'") {
|
||||||
|
$state = "char"
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
$i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
[void]$result.Append($char)
|
||||||
|
$i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state -eq "line_comment") {
|
||||||
|
if ($char -eq "`n") {
|
||||||
|
$state = "normal"
|
||||||
|
[void]$result.Append("`n")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
}
|
||||||
|
$i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state -eq "block_comment") {
|
||||||
|
if ($char -eq "*" -and $next -eq "/") {
|
||||||
|
$state = "normal"
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
$i += 2
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($char -eq "`n") {
|
||||||
|
[void]$result.Append("`n")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
}
|
||||||
|
$i += 1
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state -eq "string") {
|
||||||
|
if ($char -eq "\" -and $i + 1 -lt $length) {
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
$i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char -eq '"') {
|
||||||
|
$state = "normal"
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($char -eq "`n") {
|
||||||
|
[void]$result.Append("`n")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state -eq "char") {
|
||||||
|
if ($char -eq "\" -and $i + 1 -lt $length) {
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
$i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char -eq "'") {
|
||||||
|
$state = "normal"
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($char -eq "`n") {
|
||||||
|
[void]$result.Append("`n")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[void]$result.Append(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PackageName {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$CleanedSource
|
||||||
|
)
|
||||||
|
|
||||||
|
$pattern = "^\s*package\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*;"
|
||||||
|
$match = [regex]::Match(
|
||||||
|
$CleanedSource,
|
||||||
|
$pattern,
|
||||||
|
[System.Text.RegularExpressions.RegexOptions]::Multiline
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $match.Success) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $match.Groups[1].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-TopLevelTypes {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$CleanedSource
|
||||||
|
)
|
||||||
|
|
||||||
|
$keywords = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||||
|
[void]$keywords.Add("class")
|
||||||
|
[void]$keywords.Add("interface")
|
||||||
|
[void]$keywords.Add("enum")
|
||||||
|
[void]$keywords.Add("record")
|
||||||
|
|
||||||
|
$matches = [regex]::Matches($CleanedSource, "[A-Za-z_][A-Za-z0-9_]*|[{}]")
|
||||||
|
$depth = 0
|
||||||
|
|
||||||
|
$types = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||||
|
|
||||||
|
$i = 0
|
||||||
|
while ($i -lt $matches.Count) {
|
||||||
|
$token = $matches[$i].Value
|
||||||
|
|
||||||
|
if ($token -eq "{") {
|
||||||
|
$depth += 1
|
||||||
|
$i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token -eq "}") {
|
||||||
|
if ($depth -gt 0) {
|
||||||
|
$depth -= 1
|
||||||
|
}
|
||||||
|
$i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($depth -eq 0 -and $keywords.Contains($token)) {
|
||||||
|
if ($i + 1 -lt $matches.Count) {
|
||||||
|
$candidate = $matches[$i + 1].Value
|
||||||
|
if ([regex]::IsMatch($candidate, "^[A-Za-z_][A-Za-z0-9_]*$")) {
|
||||||
|
if ($seen.Add($candidate)) {
|
||||||
|
$types.Add($candidate)
|
||||||
|
}
|
||||||
|
$i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return $types.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ClassNamesFromFile {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$JavaFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$PackageFilters
|
||||||
|
)
|
||||||
|
|
||||||
|
$source = Get-Content -LiteralPath $JavaFile -Raw -Encoding UTF8
|
||||||
|
$cleaned = Strip-CommentsAndLiterals -Source $source
|
||||||
|
$packageName = Get-PackageName -CleanedSource $cleaned
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($packageName)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-PackageMatch -PackageName $packageName -PackageFilters $PackageFilters)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
$types = Get-TopLevelTypes -CleanedSource $cleaned
|
||||||
|
$result = [System.Collections.Generic.List[string]]::new()
|
||||||
|
|
||||||
|
foreach ($typeName in $types) {
|
||||||
|
$result.Add("$packageName.$typeName")
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ReflectionEntry {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ClassName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[bool]$IncludeDeclared,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[bool]$IncludePublic,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[bool]$IncludeConstructors,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[bool]$IncludeMethods,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[bool]$IncludeFields
|
||||||
|
)
|
||||||
|
|
||||||
|
$entry = [ordered]@{
|
||||||
|
name = $ClassName
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($IncludeDeclared) {
|
||||||
|
if ($IncludeConstructors) {
|
||||||
|
$entry["allDeclaredConstructors"] = $true
|
||||||
|
}
|
||||||
|
if ($IncludeMethods) {
|
||||||
|
$entry["allDeclaredMethods"] = $true
|
||||||
|
}
|
||||||
|
if ($IncludeFields) {
|
||||||
|
$entry["allDeclaredFields"] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($IncludePublic) {
|
||||||
|
if ($IncludeConstructors) {
|
||||||
|
$entry["allPublicConstructors"] = $true
|
||||||
|
}
|
||||||
|
if ($IncludeMethods) {
|
||||||
|
$entry["allPublicMethods"] = $true
|
||||||
|
}
|
||||||
|
if ($IncludeFields) {
|
||||||
|
$entry["allPublicFields"] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]$entry
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resolvedRoots = [System.Collections.Generic.List[string]]::new()
|
||||||
|
foreach ($root in $SourceRoot) {
|
||||||
|
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
|
||||||
|
throw "Missing source root: $root"
|
||||||
|
}
|
||||||
|
$resolvedPath = (Resolve-Path -LiteralPath $root).Path
|
||||||
|
$resolvedRoots.Add($resolvedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
$packageFilters = Parse-PackageFilters -Values $Package
|
||||||
|
|
||||||
|
$javaFiles = [System.Collections.Generic.List[string]]::new()
|
||||||
|
foreach ($root in $resolvedRoots) {
|
||||||
|
$files = Get-ChildItem -Path $root -Recurse -Filter "*.java" -File
|
||||||
|
foreach ($file in $files) {
|
||||||
|
$javaFiles.Add($file.FullName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$classNames = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$seenClassNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||||
|
|
||||||
|
foreach ($javaFile in $javaFiles) {
|
||||||
|
try {
|
||||||
|
$classes = Get-ClassNamesFromFile -JavaFile $javaFile -PackageFilters $packageFilters
|
||||||
|
foreach ($className in $classes) {
|
||||||
|
if ($seenClassNames.Add($className)) {
|
||||||
|
$classNames.Add($className)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Skipping unreadable file $javaFile : $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$finalClassNames = $classNames.ToArray()
|
||||||
|
if (-not $NoSort) {
|
||||||
|
$finalClassNames = $finalClassNames | Sort-Object
|
||||||
|
}
|
||||||
|
|
||||||
|
$reflectionEntries = [System.Collections.Generic.List[object]]::new()
|
||||||
|
foreach ($className in $finalClassNames) {
|
||||||
|
$reflectionEntries.Add(
|
||||||
|
(New-ReflectionEntry `
|
||||||
|
-ClassName $className `
|
||||||
|
-IncludeDeclared $IncludeDeclared `
|
||||||
|
-IncludePublic $IncludePublic `
|
||||||
|
-IncludeConstructors $IncludeConstructors `
|
||||||
|
-IncludeMethods $IncludeMethods `
|
||||||
|
-IncludeFields $IncludeFields)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputJson = $reflectionEntries | ConvertTo-Json -Depth 5
|
||||||
|
|
||||||
|
if ($Stdout) {
|
||||||
|
Write-Output $outputJson
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$outputPath = [System.IO.Path]::GetFullPath($Output)
|
||||||
|
$outputDir = [System.IO.Path]::GetDirectoryName($outputPath)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($outputDir) -and -not (Test-Path -LiteralPath $outputDir)) {
|
||||||
|
[void](New-Item -ItemType Directory -Path $outputDir -Force)
|
||||||
|
}
|
||||||
|
|
||||||
|
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||||
|
[System.IO.File]::WriteAllText($outputPath, $outputJson + [Environment]::NewLine, $utf8NoBom)
|
||||||
|
Write-Output "Wrote $($reflectionEntries.Count) entries to $outputPath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error $_.Exception.Message
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate a GraalVM/Quarkus reflection-config.json from Java source files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
|
||||||
|
JAVA_FILE_PATTERN = "*.java"
|
||||||
|
TYPE_KEYWORDS = {"class", "interface", "enum", "record"}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_package_filter(raw_filter: str) -> str:
|
||||||
|
normalized = raw_filter.strip()
|
||||||
|
if not normalized:
|
||||||
|
return ""
|
||||||
|
if normalized.endswith(".*"):
|
||||||
|
normalized = normalized[:-2]
|
||||||
|
return normalized.rstrip(".")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_package_filters(values: Iterable[str]) -> list[str]:
|
||||||
|
filters: list[str] = []
|
||||||
|
for value in values:
|
||||||
|
for part in value.split(","):
|
||||||
|
normalized = normalize_package_filter(part)
|
||||||
|
if normalized:
|
||||||
|
filters.append(normalized)
|
||||||
|
return list(dict.fromkeys(filters))
|
||||||
|
|
||||||
|
|
||||||
|
def package_matches(package_name: str, package_filters: list[str]) -> bool:
|
||||||
|
if not package_filters:
|
||||||
|
return True
|
||||||
|
for prefix in package_filters:
|
||||||
|
if package_name == prefix or package_name.startswith(prefix + "."):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def strip_comments_and_literals(source: str) -> str:
|
||||||
|
result: list[str] = []
|
||||||
|
i = 0
|
||||||
|
state = "normal"
|
||||||
|
length = len(source)
|
||||||
|
|
||||||
|
while i < length:
|
||||||
|
char = source[i]
|
||||||
|
nxt = source[i + 1] if i + 1 < length else ""
|
||||||
|
|
||||||
|
if state == "normal":
|
||||||
|
if char == "/" and nxt == "/":
|
||||||
|
state = "line_comment"
|
||||||
|
result.append(" ")
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if char == "/" and nxt == "*":
|
||||||
|
state = "block_comment"
|
||||||
|
result.append(" ")
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if char == '"':
|
||||||
|
state = "string"
|
||||||
|
result.append(" ")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if char == "'":
|
||||||
|
state = "char"
|
||||||
|
result.append(" ")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
result.append(char)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if state == "line_comment":
|
||||||
|
if char == "\n":
|
||||||
|
state = "normal"
|
||||||
|
result.append("\n")
|
||||||
|
else:
|
||||||
|
result.append(" ")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if state == "block_comment":
|
||||||
|
if char == "*" and nxt == "/":
|
||||||
|
state = "normal"
|
||||||
|
result.append(" ")
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
result.append("\n" if char == "\n" else " ")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if state == "string":
|
||||||
|
if char == "\\" and i + 1 < length:
|
||||||
|
result.append(" ")
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if char == '"':
|
||||||
|
state = "normal"
|
||||||
|
result.append(" ")
|
||||||
|
else:
|
||||||
|
result.append("\n" if char == "\n" else " ")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if state == "char":
|
||||||
|
if char == "\\" and i + 1 < length:
|
||||||
|
result.append(" ")
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if char == "'":
|
||||||
|
state = "normal"
|
||||||
|
result.append(" ")
|
||||||
|
else:
|
||||||
|
result.append("\n" if char == "\n" else " ")
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_package(cleaned_source: str) -> str | None:
|
||||||
|
match = re.search(
|
||||||
|
r"^\s*package\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*;",
|
||||||
|
cleaned_source,
|
||||||
|
flags=re.MULTILINE,
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_top_level_types(cleaned_source: str) -> list[str]:
|
||||||
|
tokens = list(re.finditer(r"[A-Za-z_][A-Za-z0-9_]*|[{}]", cleaned_source))
|
||||||
|
depth = 0
|
||||||
|
types: list[str] = []
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < len(tokens):
|
||||||
|
token = tokens[i].group(0)
|
||||||
|
if token == "{":
|
||||||
|
depth += 1
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if token == "}":
|
||||||
|
depth = max(0, depth - 1)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if depth == 0 and token in TYPE_KEYWORDS:
|
||||||
|
if i + 1 < len(tokens):
|
||||||
|
candidate = tokens[i + 1].group(0)
|
||||||
|
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", candidate):
|
||||||
|
types.append(candidate)
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return list(dict.fromkeys(types))
|
||||||
|
|
||||||
|
|
||||||
|
def discover_classes(java_file: Path, package_filters: list[str]) -> list[str]:
|
||||||
|
source = java_file.read_text(encoding="utf-8")
|
||||||
|
cleaned = strip_comments_and_literals(source)
|
||||||
|
package_name = extract_package(cleaned)
|
||||||
|
if not package_name or not package_matches(package_name, package_filters):
|
||||||
|
return []
|
||||||
|
classes = extract_top_level_types(cleaned)
|
||||||
|
return [f"{package_name}.{name}" for name in classes]
|
||||||
|
|
||||||
|
|
||||||
|
def collect_java_files(source_roots: list[Path]) -> list[Path]:
|
||||||
|
java_files: list[Path] = []
|
||||||
|
for root in source_roots:
|
||||||
|
java_files.extend(root.rglob(JAVA_FILE_PATTERN))
|
||||||
|
return java_files
|
||||||
|
|
||||||
|
|
||||||
|
def build_reflection_entry(
|
||||||
|
class_name: str,
|
||||||
|
include_declared: bool,
|
||||||
|
include_public: bool,
|
||||||
|
include_constructors: bool,
|
||||||
|
include_methods: bool,
|
||||||
|
include_fields: bool,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
entry: dict[str, object] = {"name": class_name}
|
||||||
|
|
||||||
|
if include_declared:
|
||||||
|
if include_constructors:
|
||||||
|
entry["allDeclaredConstructors"] = True
|
||||||
|
if include_methods:
|
||||||
|
entry["allDeclaredMethods"] = True
|
||||||
|
if include_fields:
|
||||||
|
entry["allDeclaredFields"] = True
|
||||||
|
|
||||||
|
if include_public:
|
||||||
|
if include_constructors:
|
||||||
|
entry["allPublicConstructors"] = True
|
||||||
|
if include_methods:
|
||||||
|
entry["allPublicMethods"] = True
|
||||||
|
if include_fields:
|
||||||
|
entry["allPublicFields"] = True
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def build_arg_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Scan Java source files and generate reflection-config.json "
|
||||||
|
"for selected packages."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--source-root",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Source root directory to scan (repeatable). Defaults to current directory.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--package",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help=(
|
||||||
|
"Package prefix filter, e.g. com.acme or com.acme.* "
|
||||||
|
"(repeatable or comma-separated)."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
default="reflection-config.json",
|
||||||
|
help="Output path for generated JSON (default: reflection-config.json).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--stdout",
|
||||||
|
action="store_true",
|
||||||
|
help="Print JSON to stdout instead of writing to --output.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-declared",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=True,
|
||||||
|
help="Include declared constructors/methods/fields (default: true).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-public",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=False,
|
||||||
|
help="Include public constructors/methods/fields (default: false).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-constructors",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=True,
|
||||||
|
help="Include constructor metadata (default: true).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-methods",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=True,
|
||||||
|
help="Include method metadata (default: true).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-fields",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=True,
|
||||||
|
help="Include field metadata (default: true).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-sort",
|
||||||
|
action="store_true",
|
||||||
|
help="Keep discovery order instead of sorting by class name.",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = build_arg_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
source_roots = [Path(p).resolve() for p in args.source_root] or [Path.cwd().resolve()]
|
||||||
|
missing_roots = [str(root) for root in source_roots if not root.exists()]
|
||||||
|
if missing_roots:
|
||||||
|
parser.error("Missing source roots: " + ", ".join(missing_roots))
|
||||||
|
|
||||||
|
package_filters = parse_package_filters(args.package)
|
||||||
|
|
||||||
|
class_names: list[str] = []
|
||||||
|
for java_file in collect_java_files(source_roots):
|
||||||
|
try:
|
||||||
|
class_names.extend(discover_classes(java_file, package_filters))
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
print(f"Skipping non-UTF8 file: {java_file}", file=sys.stderr)
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"Skipping unreadable file {java_file}: {exc}", file=sys.stderr)
|
||||||
|
|
||||||
|
class_names = list(dict.fromkeys(class_names))
|
||||||
|
if not args.no_sort:
|
||||||
|
class_names.sort()
|
||||||
|
|
||||||
|
reflection_entries = [
|
||||||
|
build_reflection_entry(
|
||||||
|
class_name=class_name,
|
||||||
|
include_declared=args.include_declared,
|
||||||
|
include_public=args.include_public,
|
||||||
|
include_constructors=args.include_constructors,
|
||||||
|
include_methods=args.include_methods,
|
||||||
|
include_fields=args.include_fields,
|
||||||
|
)
|
||||||
|
for class_name in class_names
|
||||||
|
]
|
||||||
|
|
||||||
|
output_json = json.dumps(reflection_entries, indent=2)
|
||||||
|
|
||||||
|
if args.stdout:
|
||||||
|
print(output_json)
|
||||||
|
else:
|
||||||
|
output_path = Path(args.output).resolve()
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path.write_text(output_json + "\n", encoding="utf-8")
|
||||||
|
print(f"Wrote {len(reflection_entries)} entries to {output_path}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Binary file not shown.
@@ -0,0 +1,78 @@
|
|||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import reflection_config_generator as rgen
|
||||||
|
|
||||||
|
|
||||||
|
class ReflectionConfigGeneratorTests(unittest.TestCase):
|
||||||
|
def test_parse_package_filters_deduplicates_and_normalizes(self) -> None:
|
||||||
|
filters = rgen.parse_package_filters(
|
||||||
|
["com.acme.*, org.foo", "com.acme", " org.foo.bar "]
|
||||||
|
)
|
||||||
|
self.assertEqual(filters, ["com.acme", "org.foo", "org.foo.bar"])
|
||||||
|
|
||||||
|
def test_extract_top_level_types_ignores_nested_types(self) -> None:
|
||||||
|
source = """
|
||||||
|
package com.acme;
|
||||||
|
|
||||||
|
public class Outer {
|
||||||
|
class Inner {}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Value(String v) {}
|
||||||
|
enum Mode { A, B }
|
||||||
|
"""
|
||||||
|
cleaned = rgen.strip_comments_and_literals(source)
|
||||||
|
self.assertEqual(
|
||||||
|
rgen.extract_top_level_types(cleaned),
|
||||||
|
["Outer", "Value", "Mode"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_main_generates_reflection_config_for_filtered_packages(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "src/com/acme").mkdir(parents=True)
|
||||||
|
(root / "src/com/acme/sub").mkdir(parents=True)
|
||||||
|
(root / "src/org/other").mkdir(parents=True)
|
||||||
|
|
||||||
|
(root / "src/com/acme/A.java").write_text(
|
||||||
|
"package com.acme; public class A {}",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(root / "src/com/acme/sub/B.java").write_text(
|
||||||
|
"package com.acme.sub; public record B(String name) {}",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(root / "src/org/other/C.java").write_text(
|
||||||
|
"package org.other; public class C {}",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
out_file = root / "reflection-config.json"
|
||||||
|
exit_code = rgen.main(
|
||||||
|
[
|
||||||
|
"--source-root",
|
||||||
|
str(root / "src"),
|
||||||
|
"--package",
|
||||||
|
"com.acme",
|
||||||
|
"--output",
|
||||||
|
str(out_file),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(exit_code, 0)
|
||||||
|
|
||||||
|
content = json.loads(out_file.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
[entry["name"] for entry in content],
|
||||||
|
["com.acme.A", "com.acme.sub.B"],
|
||||||
|
)
|
||||||
|
self.assertTrue(content[0]["allDeclaredMethods"])
|
||||||
|
self.assertTrue(content[0]["allDeclaredConstructors"])
|
||||||
|
self.assertTrue(content[0]["allDeclaredFields"])
|
||||||
|
self.assertNotIn("allPublicMethods", content[0])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user