goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,695 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.doc;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jfree.chart.JFreeChart;
import org.ta4j.core.*;
import org.ta4j.core.backtest.BarSeriesManager;
import org.ta4j.core.criteria.drawdown.MaximumDrawdownCriterion;
import org.ta4j.core.criteria.pnl.NetProfitLossCriterion;
import org.ta4j.core.indicators.RSIIndicator;
import org.ta4j.core.indicators.MACDIndicator;
import org.ta4j.core.indicators.averages.EMAIndicator;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.rules.*;
import org.ta4j.core.serialization.ComponentSerialization;
import org.ta4j.core.serialization.RuleSerialization;
import ta4jexamples.charting.workflow.ChartWorkflow;
import ta4jexamples.datasources.BitStampCsvTradesFileBarSeriesDataSource;
import java.awt.GraphicsEnvironment;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class to manage README content including chart images, code snippets,
* and documentation.
*
* <p>
* This class serves as the single source of truth for README content. It
* generates chart images, extracts code snippets from source code using special
* comment markers, and synchronizes the README with the latest code examples.
* </p>
*
* <p>
* <strong>How it works:</strong>
* <ul>
* <li>Code snippets are marked with {@code // START_SNIPPET: snippet-id} and
* {@code // END_SNIPPET: snippet-id} comments in the chart generation
* methods</li>
* <li>The README.md file contains corresponding HTML comment markers:
* {@code <!-- START_SNIPPET: snippet-id -->} and
* {@code <!-- END_SNIPPET: snippet-id -->}</li>
* <li>Run with argument "update-readme" to automatically extract snippets and
* update the README:
* {@code mvn -pl ta4j-examples exec:java -Dexec.mainClass=ta4jexamples.doc.ReadmeContentManager -Dexec.args=update-readme}</li>
* </ul>
* </p>
*
* <p>
* <strong>Usage:</strong>
* <ul>
* <li>Generate charts and update README (default):
* {@code mvn -pl ta4j-examples exec:java -Dexec.mainClass=ta4jexamples.doc.ReadmeContentManager}</li>
* <li>Update README snippets only:
* {@code mvn -pl ta4j-examples exec:java -Dexec.mainClass=ta4jexamples.doc.ReadmeContentManager -Dexec.args=update-readme}</li>
* <li>View snippets only:
* {@code mvn -pl ta4j-examples exec:java -Dexec.mainClass=ta4jexamples.doc.ReadmeContentManager -Dexec.args=snippets}</li>
* </ul>
* </p>
*/
public class ReadmeContentManager {
private static final Logger LOG = LogManager.getLogger(ReadmeContentManager.class);
/**
* Generates the EMA Crossover chart matching the README quick start example.
*
* @param outputDir the directory to save the chart image (e.g., "docs/charts"
* or ".")
* @return the path to the saved chart image, or empty if saving failed
*/
public static Optional<Path> generateEmaCrossoverChart(String outputDir) {
LOG.info("Generating EMA Crossover chart for README...");
// Load historical price data
BarSeries fullSeries = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
LOG.info("Loaded {} bars from Bitstamp series", fullSeries.getBarCount());
// Use a smaller subset for a cleaner chart (last ~150 bars)
int totalBars = fullSeries.getBarCount();
int startIndex = Math.max(0, totalBars - 150);
BarSeries series = fullSeries.getSubSeries(startIndex, totalBars);
LOG.info("Using subset: {} bars (indices {} to {})", series.getBarCount(), startIndex, totalBars - 1);
// Create indicators: calculate moving averages from close prices
ClosePriceIndicator close = new ClosePriceIndicator(series);
EMAIndicator fastEma = new EMAIndicator(close, 12); // 12-period EMA
EMAIndicator slowEma = new EMAIndicator(close, 26); // 26-period EMA
// Define entry rule: buy when fast EMA crosses above slow EMA (golden cross)
Rule entry = new CrossedUpIndicatorRule(fastEma, slowEma);
// Define exit rule: sell when price gains 3% OR loses 1.5%
Rule exit = new StopGainRule(close, 3.0) // take profit at +3%
.or(new StopLossRule(close, 1.5)); // or cut losses at -1.5%
// Combine rules into a strategy
Strategy strategy = new BaseStrategy("EMA Crossover", entry, exit);
// Run the strategy on historical data
BarSeriesManager manager = new BarSeriesManager(series);
TradingRecord record = manager.run(strategy);
LOG.info("Strategy executed: {} positions", record.getPositionCount());
// START_SNIPPET: ema-crossover
// Generate simplified chart - just price, indicators, and signals (no subchart)
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withTitle("EMA Crossover Strategy")
.withSeries(series) // Price bars (candlesticks)
.withIndicatorOverlay(fastEma) // Overlay indicators on price chart
.withIndicatorOverlay(slowEma)
.withTradingRecordOverlay(record) // Mark entry/exit points with arrows
.toChart();
chartWorkflow.saveChartImage(chart, series, "ema-crossover-strategy", "output/charts"); // Save as image
// END_SNIPPET: ema-crossover
// Display chart in interactive window (only in non-headless environments)
if (!GraphicsEnvironment.isHeadless()) {
chartWorkflow.displayChart(chart);
}
// Save chart image
Optional<Path> savedPath = chartWorkflow.saveChartImage(chart, series, "ema-crossover-readme", outputDir);
if (savedPath.isPresent()) {
Path relativePath = getRelativePath(savedPath.get());
LOG.info("Chart saved to: {}", relativePath);
} else {
LOG.warn("Failed to save ema-crossover-readme.jpg chart image");
}
return savedPath;
}
/**
* Generates an RSI Strategy chart with RSI indicator in a subchart.
* Demonstrates using subcharts for indicators with different scales.
*
* @param outputDir the directory to save the chart image
* @return the path to the saved chart image, or empty if saving failed
*/
public static Optional<Path> generateRsiStrategyChart(String outputDir) {
LOG.info("Generating RSI Strategy chart with subchart for README...");
// Load historical price data
BarSeries fullSeries = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
LOG.info("Loaded {} bars from Bitstamp series", fullSeries.getBarCount());
// Use a smaller subset for a cleaner chart (last ~150 bars)
int totalBars = fullSeries.getBarCount();
int startIndex = Math.max(0, totalBars - 150);
BarSeries series = fullSeries.getSubSeries(startIndex, totalBars);
LOG.info("Using subset: {} bars (indices {} to {})", series.getBarCount(), startIndex, totalBars - 1);
// START_SNIPPET: rsi-strategy
// Create indicators
ClosePriceIndicator close = new ClosePriceIndicator(series);
RSIIndicator rsi = new RSIIndicator(close, 14);
// RSI strategy: buy when RSI crosses below 30 (oversold), sell when RSI crosses
// above 70 (overbought)
Rule entry = new CrossedDownIndicatorRule(rsi, 30);
Rule exit = new CrossedUpIndicatorRule(rsi, 70);
Strategy strategy = new BaseStrategy("RSI Strategy", entry, exit);
TradingRecord record = new BarSeriesManager(series).run(strategy);
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withTitle("RSI Strategy with Subchart")
.withSeries(series) // Price bars (candlesticks)
.withTradingRecordOverlay(record) // Mark entry/exit points
.withSubChart(rsi) // RSI indicator in separate subchart panel
.toChart();
// END_SNIPPET: rsi-strategy
// Save chart image
Optional<Path> savedPath = chartWorkflow.saveChartImage(chart, series, "rsi-strategy-readme", outputDir);
if (savedPath.isPresent()) {
Path relativePath = getRelativePath(savedPath.get());
LOG.info("Chart saved to: {}", relativePath);
} else {
LOG.warn("Failed to save rsi-strategy-readme.jpg chart image");
}
return savedPath;
}
/**
* Generates a strategy chart with performance metrics subchart. Demonstrates
* visualizing performance analysis criteria over time.
*
* @param outputDir the directory to save the chart image
* @return the path to the saved chart image, or empty if saving failed
*/
public static Optional<Path> generateStrategyPerformanceChart(String outputDir) {
LOG.info("Generating Strategy Performance chart with metrics subchart for README...");
// Load historical price data
BarSeries fullSeries = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
LOG.info("Loaded {} bars from Bitstamp series", fullSeries.getBarCount());
// Use a smaller subset for a cleaner chart (last ~150 bars)
int totalBars = fullSeries.getBarCount();
int startIndex = Math.max(0, totalBars - 150);
BarSeries series = fullSeries.getSubSeries(startIndex, totalBars);
LOG.info("Using subset: {} bars (indices {} to {})", series.getBarCount(), startIndex, totalBars - 1);
// START_SNIPPET: strategy-performance
// Create indicators: multiple moving averages
ClosePriceIndicator close = new ClosePriceIndicator(series);
SMAIndicator sma20 = new SMAIndicator(close, 20);
EMAIndicator ema12 = new EMAIndicator(close, 12);
// Strategy: buy when EMA crosses above SMA, sell when EMA crosses below SMA
Rule entry = new CrossedUpIndicatorRule(ema12, sma20);
Rule exit = new CrossedDownIndicatorRule(ema12, sma20);
Strategy strategy = new BaseStrategy("EMA/SMA Crossover", entry, exit);
TradingRecord record = new BarSeriesManager(series).run(strategy);
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withTitle("Strategy Performance Analysis")
.withSeries(series) // Price bars (candlesticks)
.withIndicatorOverlay(sma20) // Overlay SMA on price chart
.withIndicatorOverlay(ema12) // Overlay EMA on price chart
.withTradingRecordOverlay(record) // Mark entry/exit points
.withSubChart(new MaximumDrawdownCriterion(), record) // Performance metric in subchart
.toChart();
// END_SNIPPET: strategy-performance
// Save chart image
Optional<Path> savedPath = chartWorkflow.saveChartImage(chart, series, "strategy-performance-readme",
outputDir);
if (savedPath.isPresent()) {
Path relativePath = getRelativePath(savedPath.get());
LOG.info("Chart saved to: {}", relativePath);
} else {
LOG.warn("Failed to save strategy-performance-readme.jpg chart image");
}
return savedPath;
}
/**
* Generates an advanced multi-indicator strategy chart with multiple subcharts.
* Demonstrates full charting capabilities with multiple analysis layers.
*
* @param outputDir the directory to save the chart image
* @return the path to the saved chart image, or empty if saving failed
*/
public static Optional<Path> generateAdvancedStrategyChart(String outputDir) {
LOG.info("Generating Advanced Multi-Indicator Strategy chart for README...");
// Load historical price data
BarSeries fullSeries = BitStampCsvTradesFileBarSeriesDataSource.loadBitstampSeries();
LOG.info("Loaded {} bars from Bitstamp series", fullSeries.getBarCount());
// Use a smaller subset for a cleaner chart (last ~150 bars)
int totalBars = fullSeries.getBarCount();
int startIndex = Math.max(0, totalBars - 150);
BarSeries series = fullSeries.getSubSeries(startIndex, totalBars);
LOG.info("Using subset: {} bars (indices {} to {})", series.getBarCount(), startIndex, totalBars - 1);
// START_SNIPPET: advanced-strategy
// Create indicators
ClosePriceIndicator close = new ClosePriceIndicator(series);
SMAIndicator sma50 = new SMAIndicator(close, 50);
EMAIndicator ema12 = new EMAIndicator(close, 12);
MACDIndicator macd = new MACDIndicator(close, 12, 26);
RSIIndicator rsi = new RSIIndicator(close, 14);
// Strategy: buy when EMA crosses above SMA and RSI > 50, sell when EMA crosses
// below SMA
Rule entry = new CrossedUpIndicatorRule(ema12, sma50).and(new OverIndicatorRule(rsi, 50));
Rule exit = new CrossedDownIndicatorRule(ema12, sma50);
Strategy strategy = new BaseStrategy("Advanced Multi-Indicator Strategy", entry, exit);
TradingRecord record = new BarSeriesManager(series).run(strategy);
ChartWorkflow chartWorkflow = new ChartWorkflow();
JFreeChart chart = chartWorkflow.builder()
.withTitle("Advanced Multi-Indicator Strategy")
.withSeries(series) // Price bars (candlesticks)
.withIndicatorOverlay(sma50) // Overlay SMA on price chart
.withIndicatorOverlay(ema12) // Overlay EMA on price chart
.withTradingRecordOverlay(record) // Mark entry/exit points
.withSubChart(macd) // MACD indicator in subchart
.withSubChart(rsi) // RSI indicator in subchart
.withSubChart(new NetProfitLossCriterion(), record) // Net profit/loss performance metric
.toChart();
// END_SNIPPET: advanced-strategy
// Save chart image
Optional<Path> savedPath = chartWorkflow.saveChartImage(chart, series, "advanced-strategy-readme", outputDir);
if (savedPath.isPresent()) {
Path relativePath = getRelativePath(savedPath.get());
LOG.info("Chart saved to: {}", relativePath);
} else {
LOG.warn("Failed to save advanced-strategy-readme.jpg chart image");
}
return savedPath;
}
/**
* Finds the project root directory by searching for README.md file. Starts from
* the current working directory and walks up the directory tree.
*
* @return the project root path, or current directory if not found
*/
private static Path findProjectRoot() {
Path current = Paths.get("").toAbsolutePath().normalize();
Path root = current.getRoot();
// Walk up the directory tree looking for README.md
while (current != null && !current.equals(root)) {
Path readmePath = current.resolve("README.md");
if (Files.exists(readmePath)) {
return current;
}
current = current.getParent();
}
// If not found, return current working directory as fallback
return Paths.get("").toAbsolutePath().normalize();
}
/**
* Converts an absolute path to a relative path from the project root.
*
* @param absolutePath the absolute path to convert
* @return the relative path, or the original path if conversion fails
*/
private static Path getRelativePath(Path absolutePath) {
try {
Path projectRoot = findProjectRoot();
Path normalized = absolutePath.normalize();
if (normalized.startsWith(projectRoot)) {
return projectRoot.relativize(normalized);
}
return normalized;
} catch (Exception e) {
LOG.warn("Failed to convert path to relative: {}", e.getMessage());
return absolutePath;
}
}
/**
* Generates serialization examples for the README. This method demonstrates
* serializing indicators, rules, and strategies to JSON. The examples are
* extracted via snippet markers for inclusion in the README.
*
* @param series the bar series to use for examples
*/
@SuppressWarnings("unused")
public static void generateSerializationExamples(BarSeries series) {
// START_SNIPPET: serialize-indicator
// Serialize an indicator (RSI) to JSON
ClosePriceIndicator close = new ClosePriceIndicator(series);
RSIIndicator rsi = new RSIIndicator(close, 14);
String rsiJson = rsi.toJson();
LOG.info("Output: {}", rsiJson);
// Output:
// {"type":"RSIIndicator","parameters":{"barCount":14},"components":[{"type":"ClosePriceIndicator"}]}
// END_SNIPPET: serialize-indicator
// START_SNIPPET: serialize-rule
// Serialize a rule (AndRule) to JSON
Rule rule1 = new OverIndicatorRule(rsi, 50);
Rule rule2 = new UnderIndicatorRule(rsi, 80);
Rule andRule = new AndRule(rule1, rule2);
String ruleJson = ComponentSerialization.toJson(RuleSerialization.describe(andRule));
LOG.info("Output: {}", ruleJson);
// Output:
// {"type":"AndRule","label":"AndRule","components":[{"type":"OverIndicatorRule","label":"OverIndicatorRule","components":[{"type":"RSIIndicator","parameters":{"barCount":14},"components":[{"type":"ClosePriceIndicator"}]}],"parameters":{"threshold":50.0}},{"type":"UnderIndicatorRule","label":"UnderIndicatorRule","components":[{"type":"RSIIndicator","parameters":{"barCount":14},"components":[{"type":"ClosePriceIndicator"}]}],"parameters":{"threshold":80.0}}]}
// END_SNIPPET: serialize-rule
// START_SNIPPET: serialize-strategy
// Serialize a strategy (EMA Crossover) to JSON
EMAIndicator fastEma = new EMAIndicator(close, 12);
EMAIndicator slowEma = new EMAIndicator(close, 26);
Rule entry = new CrossedUpIndicatorRule(fastEma, slowEma);
Rule exit = new CrossedDownIndicatorRule(fastEma, slowEma);
Strategy strategy = new BaseStrategy("EMA Crossover", entry, exit);
String strategyJson = strategy.toJson();
LOG.info("Output: {}", strategyJson);
// Output: {"type":"BaseStrategy","label":"EMA
// Crossover","parameters":{"unstableBars":0},"rules":[{"type":"CrossedUpIndicatorRule","label":"entry","components":[{"type":"EMAIndicator","parameters":{"barCount":12},"components":[{"type":"ClosePriceIndicator"}]},{"type":"EMAIndicator","parameters":{"barCount":26},"components":[{"type":"ClosePriceIndicator"}]}]},{"type":"CrossedDownIndicatorRule","label":"exit","components":[{"type":"EMAIndicator","parameters":{"barCount":12},"components":[{"type":"ClosePriceIndicator"}]},{"type":"EMAIndicator","parameters":{"barCount":26},"components":[{"type":"ClosePriceIndicator"}]}]}]}
// END_SNIPPET: serialize-strategy
}
/**
* Extracts code between START_SNIPPET and END_SNIPPET markers from the source
* file.
*
* @param sourceFile the Java source file to read
* @param snippetId the snippet identifier (e.g., "ema-crossover")
* @return the extracted code snippet, or empty if not found
*/
public static Optional<String> extractCodeSnippet(Path sourceFile, String snippetId) {
try {
String content = Files.readString(sourceFile, StandardCharsets.UTF_8);
String startMarker = "// START_SNIPPET: " + snippetId;
String endMarker = "// END_SNIPPET: " + snippetId;
int startIndex = content.indexOf(startMarker);
if (startIndex == -1) {
LOG.warn("Start marker not found for snippet: {}", snippetId);
return Optional.empty();
}
int endIndex = content.indexOf(endMarker, startIndex);
if (endIndex == -1) {
LOG.warn("End marker not found for snippet: {}", snippetId);
return Optional.empty();
}
// Extract code between markers (excluding the markers themselves)
int newlineIndex = content.indexOf('\n', startIndex);
if (newlineIndex == -1) {
LOG.warn("No newline found after start marker for snippet: {}", snippetId);
return Optional.empty();
}
int codeStart = newlineIndex + 1;
// Don't trim here - we need to preserve the exact indentation structure
// We'll trim only trailing whitespace from the entire snippet after processing
String snippet = content.substring(codeStart, endIndex);
// Remove leading indentation (find minimum indentation and remove it)
// Strategy: Find the minimum indentation among lines that have indentation > 0,
// but if all lines have 0 indentation, use 0. This handles cases where the
// first
// line (e.g., a comment) has no indentation but subsequent lines do.
String[] lines = snippet.split("\n");
int minIndent = Integer.MAX_VALUE;
int minIndentIncludingZero = Integer.MAX_VALUE;
boolean hasIndentedLines = false;
for (String line : lines) {
if (!line.trim().isEmpty()) {
int indent = 0;
for (char c : line.toCharArray()) {
if (c == ' ') {
indent++;
} else {
break;
}
}
minIndentIncludingZero = Math.min(minIndentIncludingZero, indent);
if (indent > 0) {
minIndent = Math.min(minIndent, indent);
hasIndentedLines = true;
}
}
}
// Use the minimum indentation of indented lines if any exist, otherwise use 0
int indentToRemove = hasIndentedLines ? minIndent
: (minIndentIncludingZero == Integer.MAX_VALUE ? 0 : minIndentIncludingZero);
// Remove minimum indentation from all lines
StringBuilder result = new StringBuilder();
for (String line : lines) {
if (line.trim().isEmpty()) {
result.append("\n");
} else {
// Ensure we don't go beyond the line length
int indentToRemoveForLine = Math.min(indentToRemove, line.length());
result.append(line.substring(indentToRemoveForLine)).append("\n");
}
}
// Trim only trailing whitespace/newlines, preserve leading structure
String resultStr = result.toString();
// Remove trailing newlines and whitespace
while (resultStr.endsWith("\n") || resultStr.endsWith(" ") || resultStr.endsWith("\r")) {
resultStr = resultStr.substring(0, resultStr.length() - 1);
}
return Optional.of(resultStr);
} catch (IOException e) {
LOG.error("Error reading source file: {}", e.getMessage());
return Optional.empty();
}
}
/**
* Updates the README file by replacing code snippets between HTML comment
* markers.
*
* @param readmePath path to the README.md file
* @param sourceFile path to the ReadmeContentManager.java file
* @return true if update was successful
*/
public static boolean updateReadmeSnippets(Path readmePath, Path sourceFile) {
try {
String readmeContent = Files.readString(readmePath, StandardCharsets.UTF_8);
String lineSeparator = detectPreferredLineSeparator(readmeContent);
String[] snippetIds = { "ema-crossover", "rsi-strategy", "strategy-performance", "advanced-strategy",
"serialize-indicator", "serialize-rule", "serialize-strategy" };
boolean updated = false;
for (String snippetId : snippetIds) {
Optional<String> snippet = extractCodeSnippet(sourceFile, snippetId);
if (snippet.isEmpty()) {
LOG.warn("Could not extract snippet: {}", snippetId);
continue;
}
String startMarker = "<!-- START_SNIPPET: " + snippetId + " -->";
String endMarker = "<!-- END_SNIPPET: " + snippetId + " -->";
// Find the code block between markers
Pattern pattern = Pattern.compile(Pattern.quote(startMarker) + ".*?" + Pattern.quote(endMarker),
Pattern.DOTALL);
String normalizedSnippet = normalizeLineSeparators(snippet.get(), lineSeparator);
String replacement = startMarker + lineSeparator + "```java" + lineSeparator + normalizedSnippet
+ lineSeparator + "```" + lineSeparator + endMarker;
String before = readmeContent;
readmeContent = pattern.matcher(readmeContent).replaceAll(Matcher.quoteReplacement(replacement));
if (!readmeContent.equals(before)) {
updated = true;
LOG.info("Updated snippet in README: {}", snippetId);
} else {
LOG.warn("Snippet markers not found in README for: {}", snippetId);
}
}
if (updated) {
Files.writeString(readmePath, readmeContent, StandardCharsets.UTF_8);
LOG.info("README updated successfully");
return true;
} else {
LOG.warn("No updates made to README");
return false;
}
} catch (IOException e) {
LOG.error("Error updating README: {}", e.getMessage());
return false;
}
}
private static String detectPreferredLineSeparator(String content) {
int crlfCount = 0;
int lfCount = 0;
for (int i = 0; i < content.length(); i++) {
if (content.charAt(i) == '\n') {
if (i > 0 && content.charAt(i - 1) == '\r') {
crlfCount++;
} else {
lfCount++;
}
}
}
if (crlfCount >= lfCount && crlfCount > 0) {
return "\r\n";
}
if (lfCount > 0) {
return "\n";
}
return System.lineSeparator();
}
private static String normalizeLineSeparators(String content, String lineSeparator) {
String normalized = content.replace("\r\n", "\n").replace("\r", "\n");
if ("\n".equals(lineSeparator)) {
return normalized;
}
return normalized.replace("\n", lineSeparator);
}
/**
* Main method to generate all README charts and automatically update README
* code snippets.
*
* <p>
* By default, this method generates all chart images and automatically updates
* the README with code snippets extracted from the chart generation methods to
* keep them in sync.
* </p>
*
* @param args optional arguments: - "update-readme": Only updates README.md
* with code snippets (no chart generation) - "snippets": Only
* prints all code snippets to log (no chart generation) -
* Otherwise: output directory for charts (defaults to
* "ta4j-examples/docs/img")
*/
public static void main(String[] args) {
if (args.length > 0 && "update-readme".equals(args[0])) {
LOG.info("=== Updating README with code snippets ===");
Path projectRoot = findProjectRoot();
Path sourceFile = projectRoot
.resolve("ta4j-examples/src/main/java/ta4jexamples/doc/ReadmeContentManager.java")
.normalize();
Path readmePath = projectRoot.resolve("README.md").normalize();
if (!Files.exists(sourceFile)) {
LOG.error("Source file not found: {}", sourceFile.toAbsolutePath());
return;
}
if (!Files.exists(readmePath)) {
LOG.error("README file not found: {}", readmePath.toAbsolutePath());
return;
}
boolean success = updateReadmeSnippets(readmePath, sourceFile);
if (success) {
LOG.info("=== README update complete ===");
} else {
LOG.warn("=== README update failed or no changes made ===");
}
return;
}
if (args.length > 0 && "snippets".equals(args[0])) {
LOG.info("=== Extracting code snippets ===");
Path projectRoot = findProjectRoot();
Path sourceFile = projectRoot
.resolve("ta4j-examples/src/main/java/ta4jexamples/doc/ReadmeContentManager.java")
.normalize();
String[] snippetIds = { "ema-crossover", "rsi-strategy", "strategy-performance", "advanced-strategy",
"serialize-indicator", "serialize-rule", "serialize-strategy" };
for (String snippetId : snippetIds) {
Optional<String> snippet = extractCodeSnippet(sourceFile, snippetId);
if (snippet.isPresent()) {
LOG.info("\n=== {} Code Snippet ===", snippetId);
LOG.info("\n{}", snippet.get());
} else {
LOG.warn("Could not extract snippet: {}", snippetId);
}
}
return;
}
// Chart generation requires a display (non-headless environment)
if (GraphicsEnvironment.isHeadless()) {
LOG.error("Cannot generate charts in headless environment. Chart generation requires a display.");
LOG.error("Use 'update-readme' or 'snippets' arguments for headless operations.");
System.exit(1);
}
String outputDir = args.length > 0 ? args[0] : "ta4j-examples/docs/img";
LOG.info("=== README Content Manager ===");
LOG.info("Output directory: {}", outputDir);
// Generate all charts
generateEmaCrossoverChart(outputDir);
generateRsiStrategyChart(outputDir);
generateStrategyPerformanceChart(outputDir);
generateAdvancedStrategyChart(outputDir);
LOG.info("=== Chart generation complete ===");
// Automatically update README with code snippets to keep them in sync
LOG.info("=== Updating README with code snippets ===");
Path projectRoot = findProjectRoot();
Path sourceFile = projectRoot.resolve("ta4j-examples/src/main/java/ta4jexamples/doc/ReadmeContentManager.java")
.normalize();
Path readmePath = projectRoot.resolve("README.md").normalize();
if (Files.exists(sourceFile) && Files.exists(readmePath)) {
boolean success = updateReadmeSnippets(readmePath, sourceFile);
if (success) {
LOG.info("=== README updated successfully ===");
} else {
LOG.warn("=== README update completed (no changes or warnings occurred) ===");
}
} else {
LOG.warn("Could not update README: source file or README not found");
LOG.warn("Source: {}, exists: {}", sourceFile.toAbsolutePath(), Files.exists(sourceFile));
LOG.warn("README: {}, exists: {}", readmePath.toAbsolutePath(), Files.exists(readmePath));
}
LOG.info("To view code snippets only, run with argument 'snippets'");
}
}
@@ -0,0 +1,768 @@
/*
* SPDX-License-Identifier: MIT
*/
package ta4jexamples.doc;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sun.source.doctree.DocCommentTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
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.DocTrees;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
/**
* Scans ta4j source trees for {@code @Deprecated(..., forRemoval = true)}
* symbols, reports release lifecycle state, and emits issue plans for release
* automation.
*
* <p>
* Typical usage:
* {@code mvn -pl ta4j-examples -am compile exec:java -Dexec.mainClass=ta4jexamples.doc.RemovalReadyDeprecationScanner -Dexec.args="--output-json build/report.json --output-md build/report.md"}
* </p>
*
* <p>
* Pass {@code --include-overdue} when a workflow target version should include
* every removal version less than or equal to that target. That keeps removal
* tracking correct when release numbers jump across major, minor, or patch
* positions.
* </p>
*
* @since 0.22.7
*/
public final class RemovalReadyDeprecationScanner {
private static final int SCHEMA_VERSION = 1;
private static final String AUTOMATION_NAMESPACE = "ta4j:deprecation-removal";
private static final String SYMBOL_TRACKING_NAMESPACE = AUTOMATION_NAMESPACE + ":symbol:v1";
private static final String GROUPED_FILE_PLAN_KIND = "grouped-file-removal";
private static final Gson PRETTY_GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create();
private static final Gson COMPACT_GSON = new GsonBuilder().disableHtmlEscaping().create();
private static final Pattern VERSION_RE = Pattern.compile("<version>\\s*([^<]+?)\\s*</version>");
private static final Pattern SEMVER_RE = Pattern.compile("\\d+\\.\\d+\\.\\d+");
private static final Pattern DECLARATION_VERSION_RE = Pattern.compile(
"(?:scheduled\\s+for\\s+removal\\s+in|removal\\s+in)\\s+(\\d+\\.\\d+\\.\\d+)", Pattern.CASE_INSENSITIVE);
private static final Pattern NOTIFIER_RE = Pattern.compile(
"DeprecationNotifier\\.warnOnce\\s*\\([^,]+,\\s*\"([^\"]+)\"(?:\\s*,\\s*\"(\\d+\\.\\d+\\.\\d+)\")?",
Pattern.DOTALL);
private static final Pattern JAVADOC_REPLACEMENT_RE = Pattern.compile("(?i)\\buse\\s+\\{@link\\s+([^}]+)\\}");
private static final Set<String> SKIP_PARTS = Set.of(".agents", ".git", ".idea", "target");
private RemovalReadyDeprecationScanner() {
}
/**
* Launches the CLI scanner.
*
* @param args command-line arguments
* @since 0.22.7
*/
public static void main(String[] args) {
int exitCode = run(args, System.out, System.err);
if (exitCode != 0) {
System.exit(exitCode);
}
}
static int run(String[] args, PrintStream stdout, PrintStream stderr) {
ScanOptions options;
try {
options = parseArgs(args);
} catch (IllegalArgumentException error) {
stderr.println("Error: " + error.getMessage());
stderr.println(usage());
return 1;
}
try {
DeprecationReport report = generateReport(options);
writeOutput(options.outputJson(), PRETTY_GSON.toJson(report));
writeOutput(options.outputMarkdown(), report.summaryMarkdown());
stdout.println(COMPACT_GSON.toJson(summaryJson(report)));
if (options.failOnDue() && report.blockingFindingCount() > 0) {
stderr.println("Removal-ready deprecation gate failed: " + report.blockingFindingCount()
+ " symbol(s) are due or overdue for " + report.removalVersion() + ".");
return 2;
}
return 0;
} catch (IOException | IllegalArgumentException error) {
stderr.println("Error: " + error.getMessage());
return 1;
}
}
private static Map<String, Object> summaryJson(DeprecationReport report) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("schemaVersion", report.schemaVersion());
summary.put("automationNamespace", report.automationNamespace());
summary.put("snapshotVersion", report.snapshotVersion());
summary.put("removalVersion", report.removalVersion());
summary.put("issuePlanCount", report.issuePlanCount());
summary.put("findingCount", report.findingCount());
summary.put("dueFindingCount", report.dueFindingCount());
summary.put("overdueFindingCount", report.overdueFindingCount());
summary.put("futureFindingCount", report.futureFindingCount());
summary.put("unscheduledFindingCount", report.unscheduledFindingCount());
summary.put("blockingFindingCount", report.blockingFindingCount());
return summary;
}
private static DeprecationReport generateReport(ScanOptions options) throws IOException {
ProjectVersion projectVersion = readProjectVersion(options.pomFile());
String targetRemovalVersion = options.targetRemovalVersion();
if (targetRemovalVersion == null) {
if (!projectVersion.version().endsWith("-SNAPSHOT")) {
throw new IllegalArgumentException("expected a SNAPSHOT version in " + options.pomFile() + ", found '"
+ projectVersion.version() + "'");
}
targetRemovalVersion = projectVersion.version().substring(0, projectVersion.version().length() - 9);
}
validateVersion(targetRemovalVersion, "--target-removal-version");
String scanTargetRemovalVersion = targetRemovalVersion;
List<SourceFinding> allFindings = scanSources(options.repoRoot());
List<IssuePlan> issuePlans = new ArrayList<>();
List<SymbolFinding> unscheduledSymbols = new ArrayList<>();
int selectedFindingCount = 0;
int dueCount = 0;
int overdueCount = 0;
int futureCount = 0;
int unscheduledCount = 0;
Map<String, List<SourceFinding>> findingsByIssue = new LinkedHashMap<>();
for (SourceFinding finding : allFindings) {
if (finding.removalVersion() == null) {
unscheduledCount++;
unscheduledSymbols.add(finding.toSymbolFinding());
continue;
}
int versionComparison = compareVersions(finding.removalVersion(), targetRemovalVersion);
if (versionComparison < 0) {
overdueCount++;
} else if (versionComparison == 0) {
dueCount++;
} else {
futureCount++;
}
boolean selected = versionComparison == 0 || options.includeOverdue() && versionComparison < 0;
if (selected) {
selectedFindingCount++;
String key = finding.removalVersion() + "|" + finding.filePath();
findingsByIssue.computeIfAbsent(key, ignored -> new ArrayList<>()).add(finding);
}
}
for (Map.Entry<String, List<SourceFinding>> entry : findingsByIssue.entrySet()) {
List<SourceFinding> findings = entry.getValue();
SourceFinding first = findings.get(0);
List<SymbolFinding> symbols = findings.stream()
.map(finding -> finding.toSymbolFinding(status(finding.removalVersion(), scanTargetRemovalVersion)))
.toList();
issuePlans.add(buildIssuePlan(projectVersion.version(), first.removalVersion(), first.filePath(),
first.module(), symbols));
}
DeprecationReport partialReport = new DeprecationReport(SCHEMA_VERSION, AUTOMATION_NAMESPACE,
OffsetDateTime.now(ZoneOffset.UTC).toString(), options.repoRoot().toString(), projectVersion.version(),
targetRemovalVersion, options.targetRemovalVersion() == null ? "snapshot" : "target",
options.includeOverdue(), options.failOnDue(), allFindings.size(), selectedFindingCount, dueCount,
overdueCount, futureCount, unscheduledCount, dueCount + overdueCount, issuePlans.size(),
List.copyOf(issuePlans), List.copyOf(unscheduledSymbols), "");
return partialReport.withSummaryMarkdown(renderMarkdown(partialReport));
}
private static List<SourceFinding> scanSources(Path repoRoot) throws IOException {
List<Path> javaFiles = iterJavaFiles(repoRoot);
if (javaFiles.isEmpty()) {
return List.of();
}
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalArgumentException("unable to locate the JDK Java compiler");
}
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null,
StandardCharsets.UTF_8)) {
Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjectsFromPaths(javaFiles);
JavacTask task = (JavacTask) compiler.getTask(null, fileManager, diagnostics,
List.of("-proc:none", "-Xlint:none"), null, fileObjects);
DocTrees docTrees = DocTrees.instance(task);
SourcePositions sourcePositions = docTrees.getSourcePositions();
List<SourceFinding> findings = new ArrayList<>();
for (CompilationUnitTree unit : task.parse()) {
Path sourcePath = sourcePath(unit);
Path relativePath = repoRoot.relativize(sourcePath);
String sourceText = Files.readString(sourcePath, StandardCharsets.UTF_8);
SourceScanner scanner = new SourceScanner(docTrees, sourcePositions, unit, sourceText,
relativePath.toString().replace('\\', '/'), moduleName(relativePath));
scanner.scan(unit, null);
findings.addAll(scanner.findings());
}
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics()
.stream()
.filter(diagnostic -> diagnostic.getKind() == Diagnostic.Kind.ERROR)
.toList();
if (!errors.isEmpty()) {
throw new IllegalArgumentException("unable to parse Java source: " + errors.get(0).getMessage(null));
}
return findings;
}
}
private static Path sourcePath(CompilationUnitTree unit) {
URI uri = unit.getSourceFile().toUri();
return Path.of(uri).toAbsolutePath().normalize();
}
private static String renderMarkdown(DeprecationReport report) {
List<String> lines = new ArrayList<>();
lines.add("# Removal-ready deprecations for " + report.snapshotVersion());
lines.add("");
lines.add("- Target removal version: " + report.removalVersion());
lines.add("- Scan mode: " + report.scanMode());
lines.add("- Planned issues: " + report.issuePlanCount());
lines.add("- Removal-ready symbols in planned issues: " + report.findingCount());
lines.add("- Due symbols: " + report.dueFindingCount());
lines.add("- Overdue symbols: " + report.overdueFindingCount());
lines.add("- Future symbols: " + report.futureFindingCount());
lines.add("- Unscheduled symbols: " + report.unscheduledFindingCount());
lines.add("");
if (report.issuePlans().isEmpty()) {
lines.add("No removal-ready deprecations matched this scan's issue-planning criteria.");
} else {
for (IssuePlan plan : report.issuePlans()) {
lines.add("## " + plan.issueTitle());
lines.add("");
lines.add("- Module: `" + plan.module() + "`");
lines.add("- File: `" + plan.filePath() + "`");
lines.add("- Removal version: `" + plan.removalVersion() + "`");
lines.add("- Symbols:");
for (SymbolFinding symbol : plan.symbols()) {
lines.add(" - " + renderSymbol(symbol));
}
lines.add("");
}
}
if (!report.unscheduledSymbols().isEmpty()) {
lines.add("");
lines.add("## Unscheduled for-removal symbols");
lines.add("");
for (SymbolFinding symbol : report.unscheduledSymbols()) {
lines.add("- `" + symbol.filePath() + "`: " + renderSymbol(symbol));
}
}
return String.join("\n", lines);
}
private static String renderSymbol(SymbolFinding symbol) {
List<String> parts = new ArrayList<>();
parts.add("`" + symbol.name() + "`");
parts.add(symbol.kind());
parts.add("line " + symbol.line());
parts.add(symbol.status());
if (symbol.replacement() != null) {
parts.add("use `" + symbol.replacement() + "`");
}
return parts.stream().reduce((left, right) -> left + ", " + right).orElse("");
}
private static ScanOptions parseArgs(String[] args) {
Path repoRoot = Path.of(".").toAbsolutePath().normalize();
String pomFile = "pom.xml";
String outputJson = null;
String outputMarkdown = null;
String targetRemovalVersion = null;
boolean includeOverdue = false;
boolean failOnDue = false;
for (int index = 0; index < args.length; index++) {
String argument = args[index];
if ("--repo-root".equals(argument)) {
repoRoot = Path.of(requireValue(argument, args, ++index)).toAbsolutePath().normalize();
} else if ("--pom-file".equals(argument)) {
pomFile = requireValue(argument, args, ++index);
} else if ("--output-json".equals(argument)) {
outputJson = requireValue(argument, args, ++index);
} else if ("--output-md".equals(argument)) {
outputMarkdown = requireValue(argument, args, ++index);
} else if ("--target-removal-version".equals(argument)) {
targetRemovalVersion = requireValue(argument, args, ++index);
validateVersion(targetRemovalVersion, argument);
} else if ("--include-overdue".equals(argument)) {
includeOverdue = true;
} else if ("--fail-on-due".equals(argument)) {
failOnDue = true;
} else {
throw new IllegalArgumentException("unknown argument " + argument);
}
}
if (outputJson == null || outputMarkdown == null) {
throw new IllegalArgumentException("--output-json and --output-md are required");
}
Path resolvedPom = Path.of(pomFile);
if (!resolvedPom.isAbsolute()) {
resolvedPom = repoRoot.resolve(resolvedPom).normalize();
}
return new ScanOptions(repoRoot, resolvedPom, Path.of(outputJson), Path.of(outputMarkdown),
targetRemovalVersion, includeOverdue, failOnDue);
}
private static String requireValue(String option, String[] args, int valueIndex) {
if (valueIndex >= args.length) {
throw new IllegalArgumentException(option + " requires a value");
}
return args[valueIndex];
}
private static ProjectVersion readProjectVersion(Path pomFile) throws IOException {
String text = Files.readString(pomFile, StandardCharsets.UTF_8);
Matcher matcher = VERSION_RE.matcher(text);
if (!matcher.find()) {
throw new IllegalArgumentException("unable to find a project version in " + pomFile);
}
return new ProjectVersion(matcher.group(1).trim());
}
private static void validateVersion(String version, String option) {
if (!SEMVER_RE.matcher(version).matches()) {
throw new IllegalArgumentException(option + " must be major.minor.patch, found '" + version + "'");
}
}
private static List<Path> iterJavaFiles(Path repoRoot) throws IOException {
List<Path> javaFiles = new ArrayList<>();
try (Stream<Path> paths = Files.walk(repoRoot)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".java"))
.filter(path -> hasMainSourceMarker(repoRoot.relativize(path)))
.filter(path -> !containsSkipPart(repoRoot.relativize(path)))
.forEach(javaFiles::add);
}
javaFiles.sort(Comparator.naturalOrder());
return javaFiles;
}
private static boolean containsSkipPart(Path relativePath) {
for (Path part : relativePath) {
if (SKIP_PARTS.contains(part.toString())) {
return true;
}
}
return false;
}
private static boolean hasMainSourceMarker(Path relativePath) {
List<String> parts = pathParts(relativePath);
for (int index = 0; index <= parts.size() - 3; index++) {
if ("src".equals(parts.get(index)) && "main".equals(parts.get(index + 1))
&& "java".equals(parts.get(index + 2))) {
return true;
}
}
return false;
}
private static String moduleName(Path relativePath) {
List<String> parts = pathParts(relativePath);
for (int index = 0; index <= parts.size() - 4; index++) {
if ("src".equals(parts.get(index + 1)) && "main".equals(parts.get(index + 2))
&& "java".equals(parts.get(index + 3))) {
return parts.get(index);
}
}
return parts.isEmpty() ? relativePath.toString() : parts.get(0);
}
private static Set<String> removalVersions(String text) {
Set<String> versions = new LinkedHashSet<>();
Matcher declarationMatcher = DECLARATION_VERSION_RE.matcher(text);
while (declarationMatcher.find()) {
versions.add(declarationMatcher.group(1));
}
Matcher notifierMatcher = NOTIFIER_RE.matcher(text);
while (notifierMatcher.find()) {
String version = notifierMatcher.group(2);
if (version != null) {
versions.add(version);
}
}
return versions;
}
private static String replacement(String text) {
Matcher notifierMatcher = NOTIFIER_RE.matcher(text);
if (notifierMatcher.find()) {
return notifierMatcher.group(1);
}
Matcher javadocMatcher = JAVADOC_REPLACEMENT_RE.matcher(text);
if (javadocMatcher.find()) {
return javadocMatcher.group(1);
}
return null;
}
private static IssuePlan buildIssuePlan(String snapshotVersion, String removalVersion, String relativePath,
String module, List<SymbolFinding> symbols) {
int symbolCount = symbols.size();
String issueTitle = symbolCount == 1
? "Remove " + removalVersion + "-ready deprecation: " + symbols.get(0).name()
: "Remove " + removalVersion + "-ready deprecations in " + Path.of(relativePath).getFileName();
String dedupeKey = removalVersion + ":" + relativePath;
String issueMarker = "<!-- ta4j:deprecation-removal version=" + removalVersion + " dedupe=" + dedupeKey
+ " -->";
String symbolLines = symbols.stream()
.map(symbol -> "- " + renderSymbol(symbol))
.reduce((left, right) -> left + "\n" + right)
.orElse("");
String issueBody = String.join("\n",
"Release automation detected removal-ready deprecations for `" + snapshotVersion + "`.", "",
"Module: `" + module + "`", "File: `" + relativePath + "`", "Removal version: `" + removalVersion + "`",
"", "Symbols:", symbolLines, "", "Acceptance checks:",
"- remove or migrate the compatibility symbols scheduled for removal in `" + removalVersion + "`",
"- update callers, tests, and documentation as needed", "- keep the full build green", "", issueMarker);
return new IssuePlan(dedupeKey, GROUPED_FILE_PLAN_KIND, issueMarker, issueTitle, issueBody, removalVersion,
module, relativePath, symbolCount, List.copyOf(symbols));
}
private static String status(String removalVersion, String targetRemovalVersion) {
int comparison = compareVersions(removalVersion, targetRemovalVersion);
if (comparison < 0) {
return "overdue";
}
if (comparison == 0) {
return "due";
}
return "future";
}
private static int compareVersions(String left, String right) {
int[] leftParts = versionParts(left);
int[] rightParts = versionParts(right);
for (int index = 0; index < leftParts.length; index++) {
int comparison = Integer.compare(leftParts[index], rightParts[index]);
if (comparison != 0) {
return comparison;
}
}
return 0;
}
private static int[] versionParts(String version) {
validateVersion(version, "version");
String[] parts = version.split("\\.");
try {
return new int[] { Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]) };
} catch (NumberFormatException error) {
throw new IllegalArgumentException("version components must fit in an integer: '" + version + "'", error);
}
}
private static void writeOutput(Path path, String content) throws IOException {
Path parent = path.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
Files.writeString(path, content, StandardCharsets.UTF_8);
}
private static List<String> pathParts(Path path) {
List<String> parts = new ArrayList<>();
for (Path part : path) {
parts.add(part.toString());
}
return parts;
}
private static String usage() {
return "Usage: --output-json <path> --output-md <path> [--repo-root <path>] [--pom-file <path>] "
+ "[--target-removal-version <major.minor.patch>] [--include-overdue] [--fail-on-due]";
}
private static final class SourceScanner extends TreePathScanner<Void, Void> {
private final DocTrees docTrees;
private final SourcePositions sourcePositions;
private final CompilationUnitTree unit;
private final String sourceText;
private final String filePath;
private final String module;
private final List<SourceFinding> findings = new ArrayList<>();
private final Deque<TypeContext> typeContexts = new ArrayDeque<>();
private int methodDepth;
private SourceScanner(DocTrees docTrees, SourcePositions sourcePositions, CompilationUnitTree unit,
String sourceText, String filePath, String module) {
this.docTrees = docTrees;
this.sourcePositions = sourcePositions;
this.unit = unit;
this.sourceText = sourceText;
this.filePath = filePath;
this.module = module;
}
private List<SourceFinding> findings() {
return findings;
}
@Override
public Void visitClass(ClassTree tree, Void unused) {
TreePath path = getCurrentPath();
CandidateDetails details = candidateDetails(path, tree.getModifiers(), tree, className(tree),
className(tree), kind(tree.getKind()), false);
Set<String> inheritedVersions = details.forRemoval() ? details.effectiveVersions() : inheritedVersions();
typeContexts.push(new TypeContext(className(tree), inheritedVersions));
try {
if (details.forRemoval()) {
findings.add(details.toFinding(filePath, module));
}
return super.visitClass(tree, unused);
} finally {
typeContexts.pop();
}
}
@Override
public Void visitMethod(MethodTree tree, Void unused) {
TreePath path = getCurrentPath();
String name = tree.getReturnType() == null ? currentTypeName() : tree.getName().toString();
String kind = tree.getReturnType() == null ? "constructor" : "method";
CandidateDetails details = candidateDetails(path, tree.getModifiers(), tree, name,
methodSignature(tree, name), kind, true);
if (details.forRemoval()) {
findings.add(details.toFinding(filePath, module));
}
methodDepth++;
try {
return super.visitMethod(tree, unused);
} finally {
methodDepth--;
}
}
@Override
public Void visitVariable(VariableTree tree, Void unused) {
TreePath path = getCurrentPath();
if (methodDepth == 0 && isField(path)) {
CandidateDetails details = candidateDetails(path, tree.getModifiers(), tree, tree.getName().toString(),
tree.getName().toString(), "field", true);
if (details.forRemoval()) {
findings.add(details.toFinding(filePath, module));
}
}
return super.visitVariable(tree, unused);
}
private CandidateDetails candidateDetails(TreePath path, ModifiersTree modifiers, Tree tree, String name,
String signature, String kind, boolean inheritMissingRemovalVersion) {
boolean forRemoval = isDeprecatedForRemoval(modifiers);
String context = context(path, tree);
Set<String> explicitVersions = removalVersions(context);
Set<String> effectiveVersions = explicitVersions.isEmpty() && inheritMissingRemovalVersion
? inheritedVersions()
: explicitVersions;
String removalVersion = effectiveVersions.stream().findFirst().orElse(null);
String findingStatus = removalVersion == null ? "unscheduled" : "scheduled";
int line = declarationLine(tree, name);
return new CandidateDetails(forRemoval, name, signature, kind, line, removalVersion, findingStatus,
replacement(context), Set.copyOf(effectiveVersions));
}
private String methodSignature(MethodTree tree, String name) {
List<String> parameterTypes = new ArrayList<>();
for (VariableTree parameter : tree.getParameters()) {
parameterTypes.add(parameter.getType().toString());
}
return name + "(" + String.join(", ", parameterTypes) + ")";
}
private String context(TreePath path, Tree tree) {
StringBuilder context = new StringBuilder();
DocCommentTree docComment = docTrees.getDocCommentTree(path);
if (docComment != null) {
context.append(docComment).append('\n');
}
context.append(sourceFragment(tree));
return context.toString();
}
private String sourceFragment(Tree tree) {
long start = sourcePositions.getStartPosition(unit, tree);
long end = sourcePositions.getEndPosition(unit, tree);
if (start < 0 || end < start || end > sourceText.length()) {
return tree.toString();
}
return sourceText.substring((int) start, (int) end);
}
private int declarationLine(Tree tree, String name) {
long start = sourcePositions.getStartPosition(unit, tree);
if (start < 0) {
return -1;
}
String fragment = sourceFragment(tree);
int nameIndex = fragment.indexOf(name);
if (nameIndex < 0) {
nameIndex = 0;
}
return (int) unit.getLineMap().getLineNumber(start + nameIndex);
}
private boolean isDeprecatedForRemoval(ModifiersTree modifiers) {
for (AnnotationTree annotation : modifiers.getAnnotations()) {
if (!isDeprecatedAnnotation(annotation)) {
continue;
}
for (ExpressionTree argument : annotation.getArguments()) {
String normalized = argument.toString().replaceAll("\\s+", "");
if (normalized.equals("forRemoval=true")) {
return true;
}
}
}
return false;
}
private boolean isDeprecatedAnnotation(AnnotationTree annotation) {
String annotationType = annotation.getAnnotationType().toString();
return "Deprecated".equals(annotationType) || "java.lang.Deprecated".equals(annotationType)
|| annotationType.endsWith(".Deprecated");
}
private Set<String> inheritedVersions() {
return typeContexts.isEmpty() ? Set.of() : typeContexts.peek().inheritedRemovalVersions();
}
private String currentTypeName() {
return typeContexts.isEmpty() ? "" : typeContexts.peek().name();
}
private boolean isField(TreePath path) {
TreePath parentPath = path.getParentPath();
return parentPath != null && parentPath.getLeaf() instanceof ClassTree;
}
private String className(ClassTree tree) {
return tree.getSimpleName().toString();
}
private String kind(Tree.Kind treeKind) {
return switch (treeKind) {
case ANNOTATION_TYPE -> "annotation";
case ENUM -> "enum";
case INTERFACE -> "interface";
case RECORD -> "record";
default -> "class";
};
}
}
private record ScanOptions(Path repoRoot, Path pomFile, Path outputJson, Path outputMarkdown,
String targetRemovalVersion, boolean includeOverdue, boolean failOnDue) {
}
private record ProjectVersion(String version) {
}
private record TypeContext(String name, Set<String> inheritedRemovalVersions) {
}
private record CandidateDetails(boolean forRemoval, String name, String signature, String kind, int line,
String removalVersion, String status, String replacement, Set<String> effectiveVersions) {
private SourceFinding toFinding(String filePath, String module) {
return new SourceFinding(name, signature, kind, line, removalVersion, status, replacement, module,
filePath);
}
}
private record SourceFinding(String name, String signature, String kind, int line, String removalVersion,
String status, String replacement, String module, String filePath) {
private SymbolFinding toSymbolFinding() {
return toSymbolFinding(status);
}
private SymbolFinding toSymbolFinding(String status) {
return new SymbolFinding(trackingKey(), name, signature, kind, line, removalVersion, status, replacement,
module, filePath);
}
private String trackingKey() {
return String.join("|", SYMBOL_TRACKING_NAMESPACE, removalVersion == null ? "unscheduled" : removalVersion,
module, filePath, kind, signature);
}
}
private record SymbolFinding(String trackingKey, String name, String signature, String kind, int line,
String removalVersion, String status, String replacement, String module, String filePath) {
}
private record IssuePlan(String dedupeKey, String planKind, String issueMarker, String issueTitle, String issueBody,
String removalVersion, String module, String filePath, int symbolCount, List<SymbolFinding> symbols) {
}
private record DeprecationReport(int schemaVersion, String automationNamespace, String generatedAt, String repoRoot,
String snapshotVersion, String removalVersion, String scanMode, boolean includeOverdue, boolean failOnDue,
int totalForRemovalCount, int findingCount, int dueFindingCount, int overdueFindingCount,
int futureFindingCount, int unscheduledFindingCount, int blockingFindingCount, int issuePlanCount,
List<IssuePlan> issuePlans, List<SymbolFinding> unscheduledSymbols, String summaryMarkdown) {
private DeprecationReport withSummaryMarkdown(String summaryMarkdown) {
return new DeprecationReport(schemaVersion, automationNamespace, generatedAt, repoRoot, snapshotVersion,
removalVersion, scanMode, includeOverdue, failOnDue, totalForRemovalCount, findingCount,
dueFindingCount, overdueFindingCount, futureFindingCount, unscheduledFindingCount,
blockingFindingCount, issuePlanCount, issuePlans, unscheduledSymbols, summaryMarkdown);
}
}
}