diff --git a/RegexpHeader/Example2/extra-config-files.txt b/RegexpHeader/Example2/extra-config-files.txt
new file mode 100644
index 000000000..93925e67c
--- /dev/null
+++ b/RegexpHeader/Example2/extra-config-files.txt
@@ -0,0 +1 @@
+java.header
\ No newline at end of file
diff --git a/extractor/config/checkstyle/suppressions.xml b/extractor/config/checkstyle/suppressions.xml
index 29b6125fd..44e324890 100644
--- a/extractor/config/checkstyle/suppressions.xml
+++ b/extractor/config/checkstyle/suppressions.xml
@@ -11,4 +11,6 @@
+
+
diff --git a/extractor/config/pmd/pmd-ruleset.xml b/extractor/config/pmd/pmd-ruleset.xml
index 79e0c4798..bd8be0522 100644
--- a/extractor/config/pmd/pmd-ruleset.xml
+++ b/extractor/config/pmd/pmd-ruleset.xml
@@ -57,6 +57,9 @@
+
+
diff --git a/extractor/src/main/java/com/example/extractor/CheckstyleExampleExtractor.java b/extractor/src/main/java/com/example/extractor/CheckstyleExampleExtractor.java
index de14e8acd..86570ff8c 100644
--- a/extractor/src/main/java/com/example/extractor/CheckstyleExampleExtractor.java
+++ b/extractor/src/main/java/com/example/extractor/CheckstyleExampleExtractor.java
@@ -82,35 +82,29 @@ public final class CheckstyleExampleExtractor {
/** The subfolder name for all-in-one examples. */
private static final String ALL_IN_ONE_SUBFOLDER = "all-examples-in-one";
- /**
- * Number of expected arguments when processing a single input file.
- */
- private static final int SINGLE_INPUT_FILE_ARG_COUNT = 5;
+ /** The buffer size for reading and writing files. */
+ private static final int BUFFER_SIZE = 1024;
- /**
- * Index of the "--input-file" flag in the argument array.
- */
- private static final int INPUT_FILE_FLAG_INDEX = 1;
+ /** Number of expected arguments when processing a configuration file. */
+ private static final int CONFIG_FILE_ARG_COUNT = 5;
- /**
- * Index of the input file path in the argument array.
- */
+ /** Index of the "--config-file" flag in the argument array. */
+ private static final int CONFIG_FILE_FLAG_INDEX = 1;
+
+ /** Index of the configuration file path in the argument array. */
+ private static final int CONFIG_FILE_PATH_INDEX = 2;
+
+ /** Index of the input file path in the argument array. */
private static final int INPUT_FILE_PATH_INDEX = 2;
- /**
- * Index of the output file path in the argument array.
- */
- private static final int OUTPUT_FILE_PATH_INDEX = 3;
+ /** Index of the output file path in the argument array. */
+ private static final int CONFIG_OUTPUT_PATH_INDEX = 3;
- /**
- * Index of the output file path in the argument array.
- */
- private static final int PROJECT_OUTPUT_PATH_INDEX = 4;
+ /** Index of the projects output path in the argument array. */
+ private static final int CONFIG_PROJECTS_PATH_INDEX = 4;
- /**
- * The buffer size for reading and writing files.
- */
- private static final int BUFFER_SIZE = 1024;
+ /** The configuration path variable placeholder. */
+ private static final String CONFIG_PATH_VARIABLE = "${config.path}";
/**
* Private constructor to prevent instantiation of this utility class.
@@ -124,31 +118,23 @@ private CheckstyleExampleExtractor() {
*
* @param args Command line arguments
* @throws Exception If an error occurs during processing
- * @throws IllegalArgumentException if the argument is invalid.
+ * @throws IllegalArgumentException if the argument is invalid
*/
public static void main(final String[] args) throws Exception {
if (args.length < 1) {
throw new IllegalArgumentException(
- "Usage: [--input-config "
- + "]"
+ "Usage: [--input-file "
+ + " ] "
+ + "or [--config-file "
+ + " ]"
);
}
- if (args.length == SINGLE_INPUT_FILE_ARG_COUNT
- && "--input-file".equals(args[INPUT_FILE_FLAG_INDEX])) {
- // New functionality: process single input file
- final String inputFilePath = args[INPUT_FILE_PATH_INDEX];
- final String configOutputPath = args[OUTPUT_FILE_PATH_INDEX];
- final String projectsOutputPath = args[PROJECT_OUTPUT_PATH_INDEX];
-
- // Process input file and generate config
- processInputFile(Paths.get(inputFilePath), Paths.get(configOutputPath));
-
- // Output default projects list
- outputDefaultProjectsList(projectsOutputPath);
+ if (args.length >= CONFIG_FILE_ARG_COUNT) {
+ processCommandLineOption(args);
}
else {
- // Functionality: process all examples
+ // Original functionality: process all examples
final String checkstyleRepoPath = args[0];
final List allExampleDirs = findAllExampleDirs(checkstyleRepoPath);
final Map> moduleExamples = processExampleDirs(allExampleDirs);
@@ -162,6 +148,232 @@ public static void main(final String[] args) throws Exception {
}
}
+ /**
+ * Process command line options.
+ *
+ * @param args Command line arguments
+ * @throws Exception If an error occurs during processing
+ * @throws IllegalArgumentException if an unknown option is provided
+ */
+ private static void processCommandLineOption(final String... args) throws Exception {
+ final String option = args[CONFIG_FILE_FLAG_INDEX];
+ processOption(option, args);
+ }
+
+ /**
+ * Process the given option with arguments.
+ *
+ * @param option The option to process
+ * @param args Command line arguments
+ * @throws IllegalArgumentException If an unknown option
+ * @throws Exception If an error occurs during processing
+ */
+ private static void processOption(final String option, final String... args) throws Exception {
+ switch (option) {
+ case "--input-file":
+ processInputFileOption(args);
+ break;
+ case "--config-file":
+ processConfigFileOption(args);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown option: " + option);
+ }
+ }
+
+ /**
+ * Process input file option.
+ *
+ * @param args Command line arguments
+ * @throws Exception If an error occurs during processing
+ */
+ private static void processInputFileOption(final String... args) throws Exception {
+ final Path inputFile = Paths.get(args[INPUT_FILE_PATH_INDEX]);
+ final Path configOutputFile = Paths.get(args[CONFIG_OUTPUT_PATH_INDEX]);
+ final Path projectsOutputFile = Paths.get(args[CONFIG_PROJECTS_PATH_INDEX]);
+
+ processInputFile(inputFile, configOutputFile);
+ outputDefaultProjectsList(projectsOutputFile.toString());
+ }
+
+ /**
+ * Process config file option.
+ *
+ * @param args Command line arguments
+ * @throws Exception If an error occurs during processing
+ */
+ private static void processConfigFileOption(final String... args) throws Exception {
+ final Path configFile = Paths.get(args[CONFIG_FILE_PATH_INDEX]);
+ final Path configOutputFile = Paths.get(args[CONFIG_OUTPUT_PATH_INDEX]);
+ final Path projectsOutputFile = Paths.get(args[CONFIG_PROJECTS_PATH_INDEX]);
+
+ processConfigFile(configFile, configOutputFile);
+ outputDefaultProjectsList(projectsOutputFile.toString());
+ }
+
+ /**
+ * Processes external files referenced in the configuration content.
+ *
+ * @param configContent The original configuration content.
+ * @param configFile The path to the configuration file.
+ * @param outputFile The path to the output file.
+ * @return The updated configuration content with paths replaced.
+ * @throws IOException If an I/O error occurs during file operations.
+ */
+ private static String processExternalFilesInConfig(
+ final String configContent,
+ final Path configFile,
+ final Path outputFile) throws IOException {
+ final Pattern pattern = Pattern.compile("");
+ final Matcher matcher = pattern.matcher(configContent);
+ String processedContent = configContent;
+ while (matcher.find()) {
+ final String propertyValue = matcher.group(2);
+ if (isExternalProperty(propertyValue)) {
+ processedContent = processProperty(propertyValue, processedContent, configFile,
+ outputFile);
+ }
+ }
+ return processedContent;
+ }
+
+ /**
+ * Checks if the property value references an external file.
+ *
+ * @param propertyValue The value of the property to check.
+ * @return True if the property references an external file; false otherwise.
+ */
+ private static boolean isExternalProperty(final String propertyValue) {
+ return propertyValue.contains("config/java.header")
+ || propertyValue.contains("${execution.path}");
+ }
+
+ /**
+ * Processes a single property by replacing paths and copying external files.
+ *
+ * @param propertyValue The original property value.
+ * @param content The current configuration content.
+ * @param configFile The path to the configuration file.
+ * @param outputFile The path to the output file.
+ * @return The updated configuration content.
+ * @throws IOException If an I/O error occurs.
+ */
+ private static String processProperty(
+ final String propertyValue,
+ final String content,
+ final Path configFile,
+ final Path outputFile) throws IOException {
+ // Replace the path with ${config.path}
+ final String newPropertyValue = propertyValue
+ .replace("${execution.path}", CONFIG_PATH_VARIABLE)
+ .replace("config/java.header", CONFIG_PATH_VARIABLE + "/java.header");
+
+ // Update the content
+ final String updatedContent = content.replace(propertyValue, newPropertyValue);
+
+ // Copy the external file to the output directory
+ copyExternalFile(configFile, outputFile);
+
+ return updatedContent;
+ }
+
+ /**
+ * Copies the external file referenced in the configuration to the output directory.
+ *
+ * @param configFile The path to the configuration file.
+ * @param outputFile The path to the output file.
+ * @throws IOException If an I/O error occurs during file operations.
+ */
+ private static void copyExternalFile(final Path configFile, final Path outputFile)
+ throws IOException {
+ final Path configParent = getParentPathOrWarn(configFile, "Config file");
+ final Path outputParent = getParentPathOrWarn(outputFile, "Output file");
+ processParentPaths(configParent, outputParent);
+ }
+
+ /**
+ * Process parent paths and copy files if valid.
+ *
+ * @param configParent The parent path of the config file
+ * @param outputParent The parent path of the output file
+ * @throws IOException If an I/O error occurs
+ */
+ private static void processParentPaths(final Path configParent, final Path outputParent)
+ throws IOException {
+ if (configParent == null || outputParent == null) {
+ handleNullParentPath("Parent directory");
+ }
+ else {
+ final Path sourceHeaderFile = configParent.resolve("java.header");
+ final Path targetHeaderFile = outputParent.resolve("config/java.header");
+ copyFileIfExists(sourceHeaderFile, targetHeaderFile);
+ }
+ }
+
+ /**
+ * Copies a file if it exists.
+ *
+ * @param sourceFile The source file path.
+ * @param targetFile The target file path.
+ * @throws IOException If an I/O error occurs during file operations.
+ * @throws IllegalArgumentException if target file has no parent directory
+ */
+ private static void copyFileIfExists(final Path sourceFile, final Path targetFile)
+ throws IOException {
+ if (Files.exists(sourceFile)) {
+ final Path targetParent = targetFile.getParent();
+ if (targetParent == null) {
+ throw new IllegalArgumentException("Target file must have a parent directory: "
+ + targetFile);
+ }
+ Files.createDirectories(targetParent);
+ Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
+ }
+ else {
+ LOGGER.warning("External file not found: " + sourceFile);
+ }
+ }
+
+ /**
+ * Handles null parent path case by logging a warning.
+ *
+ * @param fileType The type of file (for logging purposes)
+ */
+ private static void handleNullParentPath(final String fileType) {
+ LOGGER.warning(fileType + " has no parent directory");
+ }
+
+ /**
+ * Processes a configuration file and generates an output file.
+ *
+ * @param configFile The path to the configuration file
+ * @param outputFile The path to the output file
+ * @throws Exception If an error occurs during processing
+ * @throws IOException If an error occurs during processing
+ */
+ public static void processConfigFile(
+ final Path configFile,
+ final Path outputFile)
+ throws Exception {
+ // Check if the config file exists
+ if (!Files.exists(configFile)) {
+ LOGGER.severe("Config file does not exist: " + configFile);
+ throw new IOException("Config file does not exist: " + configFile);
+ }
+
+ // Read the config file content
+ final String configContent = Files.readString(configFile, StandardCharsets.UTF_8);
+
+ // Process external files in the config content
+ final String updatedContent = processExternalFilesInConfig(
+ configContent, configFile, outputFile);
+
+ // Write the updated content to the output file
+ Files.writeString(outputFile, updatedContent, StandardCharsets.UTF_8);
+
+ LOGGER.info("Generated configuration at " + outputFile);
+ }
+
/**
* Writes the default projects list to the specified file.
*
@@ -794,4 +1006,19 @@ private static int extractExampleNumber(final String filename) {
return exampleNumber;
}
+
+ /**
+ * Retrieves the parent path of a given path, logging a warning if it is null.
+ *
+ * @param path The path to get the parent of
+ * @param description A description used in the warning message
+ * @return The parent path, or null if it does not exist
+ */
+ private static Path getParentPathOrWarn(final Path path, final String description) {
+ final Path parent = path.getParent();
+ if (parent == null) {
+ LOGGER.warning(description + " has no parent directory: " + path);
+ }
+ return parent;
+ }
}
diff --git a/extractor/src/test/java/com/example/extractor/MainsLauncherTest.java b/extractor/src/test/java/com/example/extractor/MainsLauncherTest.java
index 55f95e4d1..230e2a1e5 100644
--- a/extractor/src/test/java/com/example/extractor/MainsLauncherTest.java
+++ b/extractor/src/test/java/com/example/extractor/MainsLauncherTest.java
@@ -31,6 +31,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -40,21 +41,27 @@
*/
class MainsLauncherTest {
- /**
- * The base path to the Checkstyle repository.
- */
+ /** The base path to the Checkstyle repository. */
private static final String CHECKSTYLE_REPO_PATH = "../.ci-temp/checkstyle";
- /**
- * Base path for Checkstyle checks package used in test resource files.
- */
+ /** Base path for Checkstyle checks package used in test resource files. */
private static final String CHECKSTYLE_CHECKS_BASE_PATH =
"com/puppycrawl/tools/checkstyle/checks";
+ /** The constant for default projects file. */
+ private static final String DEFAULT_PROJECTS_FILE = "list-of-projects.yml";
+
+ /** Temporary folder for test files. */
+ @TempDir
+ Path temporaryFolder;
+
/**
- * The constant for default projects file.
+ * Resets system properties after each test.
*/
- private static final String DEFAULT_PROJECTS_FILE = "list-of-projects.yml";
+ @AfterEach
+ void tearDown() {
+ System.clearProperty("config.path");
+ }
/**
* Tests the main method of CheckstyleExampleExtractor.
@@ -122,7 +129,6 @@ private String loadDefaultProjectsList() throws IOException {
/**
* Tests the getTemplateFilePathForInputFile method.
- *
*/
@Test
void testGetTemplateFilePathForInputFile() throws Exception {
@@ -139,8 +145,81 @@ void testGetTemplateFilePathForInputFile() throws Exception {
assertThat(templatePath).endsWith("config-template-treewalker.xml");
}
+ /**
+ * Helper method to load a file's content into a string.
+ *
+ * @param filePath The path to the file.
+ * @return The file's content as a string.
+ * @throws IOException If an I/O error occurs.
+ */
private String loadToString(final String filePath) throws IOException {
final byte[] encoded = Files.readAllBytes(Paths.get(filePath));
return new String(encoded, StandardCharsets.UTF_8);
}
+
+ /**
+ * Tests the external config files functionality.
+ */
+ @Test
+ void testMainWithExternalConfigFiles(@TempDir final Path tempDir) throws Exception {
+ // Create the 'config' directory inside the tempDir
+ final Path configDir = tempDir.resolve("config");
+ Files.createDirectories(configDir);
+
+ // Write the java.header file inside the 'config' directory
+ final String headerFileContent = """
+ ^// Copyright \\(C\\) (\\d\\d\\d\\d -)? 2004 MyCompany$
+ ^// All rights reserved$
+ """;
+
+ final Path headerFile = configDir.resolve("java.header");
+ Files.writeString(headerFile, headerFileContent);
+
+ // Write the config.xml file with ${config.path}
+ final String configXmlContent = """
+
+
+
+
+
+
+
+
+ """;
+
+ final Path configXmlFile = tempDir.resolve("config.xml");
+ Files.writeString(configXmlFile, configXmlContent);
+
+ // Set the system property before running the test
+ System.setProperty("config.path", tempDir.resolve("config").toAbsolutePath().toString());
+
+ final Path outputConfigFile = tempDir.resolve("output-config.xml");
+ final Path outputProjectsFile = tempDir.resolve("output-projects.yml");
+
+ assertDoesNotThrow(() -> {
+ CheckstyleExampleExtractor.main(new String[]{
+ CHECKSTYLE_REPO_PATH,
+ "--config-file",
+ configXmlFile.toString(),
+ outputConfigFile.toString(),
+ outputProjectsFile.toString(),
+ });
+ });
+
+ assertTrue(Files.exists(outputConfigFile), "Config output file should be created");
+
+ final String generatedConfigContent = Files.readString(outputConfigFile);
+ assertThat(generatedConfigContent)
+ .contains("");
+
+ // Verify that the header file was copied to the config directory
+ assertTrue(Files.exists(outputConfigFile.getParent().resolve("config/java.header")),
+ "Header file should be copied to config directory");
+
+ assertTrue(Files.exists(outputProjectsFile), "Projects output file should be created");
+ final String generatedProjectsContent = Files.readString(outputProjectsFile);
+ assertFalse(generatedProjectsContent.isEmpty(), "Projects file should not be empty");
+ }
}