From 4351c8a1ff25f314863c04dc019c24e48034c30e Mon Sep 17 00:00:00 2001 From: piyush kumar sadangi Date: Tue, 12 Nov 2024 23:16:10 +0530 Subject: [PATCH] Issue 214: Support of config bundles with extra configuration files --- RegexpHeader/Example2/extra-config-files.txt | 1 + extractor/config/checkstyle/suppressions.xml | 2 + extractor/config/pmd/pmd-ruleset.xml | 3 + .../extractor/CheckstyleExampleExtractor.java | 305 +++++++++++++++--- .../example/extractor/MainsLauncherTest.java | 97 +++++- 5 files changed, 360 insertions(+), 48 deletions(-) create mode 100644 RegexpHeader/Example2/extra-config-files.txt 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"); + } }