diff --git a/README.md b/README.md index 40aca81..e98bf74 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,11 @@ public class FooToBarMigration extends AbstractMigration { } ``` +#### Leveraging utilities classes +The following Java classes in `org.technologybrewery.baton.util` can be leveraged to easily implement common migration logic into your extension: +* `CommonUtils` +* `FileUtils` + ### Configure Baton to use the migration With a migration to apply, we both configure and tailor that use through a simple json file. This file can live anywhere in Baton's classpath and is named `migrations.json` by default. diff --git a/baton-maven-plugin/pom.xml b/baton-maven-plugin/pom.xml index 45c25ba..7857763 100644 --- a/baton-maven-plugin/pom.xml +++ b/baton-maven-plugin/pom.xml @@ -43,6 +43,11 @@ slf4j-api 2.0.5 + + com.vdurmont + semver4j + 3.1.0 + org.apache.maven.shared file-management diff --git a/baton-maven-plugin/src/main/java/org/technologybrewery/baton/util/CommonUtils.java b/baton-maven-plugin/src/main/java/org/technologybrewery/baton/util/CommonUtils.java new file mode 100644 index 0000000..f5ff2e7 --- /dev/null +++ b/baton-maven-plugin/src/main/java/org/technologybrewery/baton/util/CommonUtils.java @@ -0,0 +1,16 @@ +package org.technologybrewery.baton.util; +import com.vdurmont.semver4j.Semver; + +public class CommonUtils { + /** + * Checks if version1 is less than version2 using the Semver4j library. + * + * @param version1 + * @param version2 + * @return isLessThanVersion - if true, version1 is less than version2. + */ + public static boolean isLessThanVersion(String version1, String version2) { + Semver sem = new Semver(version1); + return sem.isLowerThan(version2); + } +} diff --git a/baton-maven-plugin/src/main/java/org/technologybrewery/baton/util/FileUtils.java b/baton-maven-plugin/src/main/java/org/technologybrewery/baton/util/FileUtils.java new file mode 100644 index 0000000..bee8593 --- /dev/null +++ b/baton-maven-plugin/src/main/java/org/technologybrewery/baton/util/FileUtils.java @@ -0,0 +1,204 @@ +package org.technologybrewery.baton.util; + +import org.apache.commons.io.IOUtils; +import org.codehaus.plexus.util.StringUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class FileUtils { + + /** + * + * @param file the File + * @param toReplace a string representing the text to replace + * @param replacement the replacement text to substitute the toReplace string + * @return a boolean set to true if at least one replacement was performed in the file + */ + public static boolean replaceLiteralInFile(File file, String toReplace, String replacement) throws IOException { + boolean replacedInFile = false; + + if (file != null && file.exists()) { + String content = new String(Files.readAllBytes((file.toPath()))); + content = content.replace(toReplace, replacement); + Files.write(file.toPath(), content.getBytes()); + replacedInFile = true; + } + return replacedInFile; + } + + /** + * + * @param file the File + * @param regex a regex representing the text to replace, as a String + * @param replacement the replacement text to substitute the regex + * @return a boolean set to true if at least one replacement was performed in the file + */ + public static boolean replaceInFile(File file, String regex, String replacement) throws IOException { + boolean replacedInFile = false; + if (file != null && file.exists()) { + Charset charset = StandardCharsets.UTF_8; + String fileContent = new String(Files.readAllBytes(file.toPath()), charset); + + Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE); + Matcher matcher = pattern.matcher(fileContent); + String newFileContent = matcher.replaceAll(replacement); + IOUtils.write(newFileContent, new FileOutputStream(file), charset); + replacedInFile = true; + } + return replacedInFile; + } + + /** + * Evaluates a file against a regex pattern and replaces a substring of each regex match + * with a specified replacement + * @param file the File + * @param regex a regex representing the text to replace a substring of + * @param substring the substring of the regex match that will be replaced + * @param replacement the replacement of the match substring + * @return a boolean set to true if at least one modification was performed in the file + */ + public static boolean modifyRegexMatchInFile(File file, String regex, String substring, String replacement) { + if (file == null || !file.exists()) { + return false; + } + + boolean modified = false; + + try { + Path path = file.toPath(); + Charset charset = StandardCharsets.UTF_8; + List resultLines = new ArrayList<>(); + + Pattern pattern = Pattern.compile(regex); + for (String line : Files.readAllLines(path, charset)) { + if (pattern.matcher(line).find()) { + line = line.replace(substring, replacement); + modified = true; + } + resultLines.add(line); + } + if (modified) { + Files.write(path, resultLines, charset); + } + } catch (IOException e) { + return false; + } + return modified; + } + + /** + * Reads in the {@link File} object and returns a {@link List} of the contents. + * @param file {@link File} to read + * @return {@link List} of the contents + * @throws IOException + */ + public static List readAllFileLines(File file) throws IOException { + return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); + } + + /** + * Writes a {@link List} of the contents to the {@link File} object. + * @param file {@link File} to write + * @param contents {@link List} of the contents + * @throws IOException + */ + public static void writeFile(File file, List contents) throws IOException { + Files.write(file.toPath(), contents, StandardCharsets.UTF_8); + } + + /** + * @see FileUtils#getRegExCaptureGroups(String, String) + * @param regex a regex containing capture groups, as a String + * @param file the file to search for matching capture groups + * @return An ArrayList of Strings representing each capture group in the regex that was matched + */ + public static ArrayList getRegExCaptureGroups(String regex, File file) throws IOException { + String fileContent = ""; + if (file != null && file.exists()) { + Charset charset = StandardCharsets.UTF_8; + fileContent = new String(Files.readAllBytes(file.toPath()), charset); + } + return StringUtils.isNotEmpty(fileContent) ? getRegExCaptureGroups(regex, fileContent) : new ArrayList<>(); + } + + /** + * + * @param regex a regex containing capture groups, as a String + * @param input the string to search for matching capture groups + * @return An ArrayList of Strings representing each capture group in the regex that was matched + */ + public static ArrayList getRegExCaptureGroups(String regex, String input) { + Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE); + Matcher matcher = pattern.matcher(input); + + ArrayList captured = new ArrayList<>(); + if (matcher.find()) { + // Skip the 0 index -- the first match is always all capture groups put together + for (int i = 1; i <= matcher.groupCount(); ++i) { + captured.add(matcher.group(i)); + } + } + + return captured; + } + + /** + * Evaluates a regex pattern against a file to determine if at least one regex match exists + * + * @param regex a regex pattern, as a String + * @param file the file to search for matching substrings + * @return true if there is at least one regex match, otherwise false + */ + public static boolean hasRegExMatch(String regex, File file) throws IOException { + String fileContent; + if (file != null && file.exists()) { + Charset charset = StandardCharsets.UTF_8; + fileContent = Files.readString(file.toPath(), charset); + return Pattern.compile(regex, Pattern.MULTILINE).matcher(fileContent).find(); + } else { + return false; + } + } + + /** + * Infers the indentation style from the given line. + * + * @param line the line to infer the indentation style from + * @param level the level of indentation of the line + * @return a single indent in the inferred style + */ + public static String getIndent(String line, int level) { + if( level < 1 ) { + return ""; + } + int i = 0; + while (i < line.length() && Character.isWhitespace(line.charAt(i))) { + i++; + } + return line.substring(0, i/level); + } + + /** + * Indent the values the desired number of tabs with a variable tab size. + * @param values List of {@link String} values to indent + * @param numSpaces number of spaces to indent + */ + public static void indentValues(List values, int numSpaces) { + String SPACE = " "; + for (int i = 0; i < values.size(); i++) { + if (!values.get(i).isBlank()) { + values.set(i, SPACE.repeat(numSpaces) + values.get(i)); + } + } + } +} diff --git a/baton-maven-plugin/src/test/java/org/technologybrewery/baton/UtilsTestSteps.java b/baton-maven-plugin/src/test/java/org/technologybrewery/baton/UtilsTestSteps.java new file mode 100644 index 0000000..57c0bb6 --- /dev/null +++ b/baton-maven-plugin/src/test/java/org/technologybrewery/baton/UtilsTestSteps.java @@ -0,0 +1,89 @@ +package org.technologybrewery.baton; + +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import org.technologybrewery.baton.util.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class UtilsTestSteps { + + private static final File DEFAULT_TEST_DIRECTORY = new File("target/utils-test/"); + private static final String DEFAULT_TEST_FILE_NAME = "text_file.txt"; + File testFile; + Boolean match; + String inputAsString; + List captureGroups; + + @Given("I have a string containing {string}") + public void i_have_a_string_containing(String str) throws Throwable { + inputAsString = str; + } + + @When("I use the regex {string} to retrieve capture groups") + public void i_use_the_regex_to_retrieve_capture_groups(String regex) throws IOException { + if (testFile != null) { + captureGroups = FileUtils.getRegExCaptureGroups(regex, testFile); + } else { + captureGroups = FileUtils.getRegExCaptureGroups(regex, inputAsString); + } + } + + @Then("the size of the retrieved capture groups should be \"{int}\"") + public void the_size_of_the_retrieved_capture_groups_should_be(int groups) { + assertEquals(groups, captureGroups.size(),"Unexpected size for capture groups"); + } + + @Then("the capture groups should include") + public void the_capture_groups_should_include(List expectedGroups) { + assertEquals(expectedGroups, captureGroups); + } + + @Given("I have a file containing the string {string}") + public void i_have_a_file_containing_the_string(String string) throws IOException { + testFile = new File(DEFAULT_TEST_DIRECTORY, DEFAULT_TEST_FILE_NAME); + if (DEFAULT_TEST_DIRECTORY.mkdirs()) { + FileUtils.writeFile(testFile, Collections.singletonList(string)); + } + } + + @When("I use the regex {string} to substitute {string}") + public void i_use_the_regex_to_substitute(String regex, String substitute) throws IOException { + FileUtils.replaceInFile(testFile, regex, substitute); + } + + @When("I use the literal {string} to substitute {string}") + public void iUseTheLiteralToSubstitute(String literal, String substitute) throws IOException { + FileUtils.replaceLiteralInFile(testFile, literal, substitute); + } + + @Then("the file should now contain the string {string}") + public void the_file_should_now_contain_the_string(String string) throws IOException { + assertTrue(FileUtils.hasRegExMatch(string, testFile), String.format("File does not contain expected string %s",string)); + } + + @When("I use the regex {string} to substitute {string} with {string}") + public void i_use_the_regex_to_substitute_with(String regex, String target, String substitute) { + FileUtils.modifyRegexMatchInFile(testFile, regex, target, substitute); + } + + @When("I use the regex {string} to search for matches in a file") + public void i_use_the_regex_to_search_for_matches_in_a_file(String regex) throws IOException { + match = FileUtils.hasRegExMatch(regex, testFile); + + } + + @Then("the match result should be \"{booleanValue}\"") + public void the_match_result_should_be(Boolean expected) { + assertEquals(expected, match, "RegEx file matcher did not return expected result"); + + } +} diff --git a/baton-maven-plugin/src/test/java/org/technologybrewery/baton/util/TestUtils.java b/baton-maven-plugin/src/test/java/org/technologybrewery/baton/util/TestUtils.java new file mode 100644 index 0000000..19ea63e --- /dev/null +++ b/baton-maven-plugin/src/test/java/org/technologybrewery/baton/util/TestUtils.java @@ -0,0 +1,11 @@ + +package org.technologybrewery.baton.util; + +import io.cucumber.java.ParameterType; + +public class TestUtils { + @ParameterType( value = "true|True|TRUE|false|False|FALSE") + public Boolean booleanValue(String value){ + return Boolean.valueOf(value); + } +} diff --git a/baton-maven-plugin/src/test/resources/specifications/util/common-utils.feature b/baton-maven-plugin/src/test/resources/specifications/util/common-utils.feature new file mode 100644 index 0000000..8eef7b5 --- /dev/null +++ b/baton-maven-plugin/src/test/resources/specifications/util/common-utils.feature @@ -0,0 +1,2 @@ +Feature: Baton utilities can be used to assist with commonly used logic in a migration + Scenario: I can easily compare the semantic versioning of two strings diff --git a/baton-maven-plugin/src/test/resources/specifications/util/file-utils.feature b/baton-maven-plugin/src/test/resources/specifications/util/file-utils.feature new file mode 100644 index 0000000..e4a1628 --- /dev/null +++ b/baton-maven-plugin/src/test/resources/specifications/util/file-utils.feature @@ -0,0 +1,45 @@ +@baton +Feature: Baton utilities can be used to assist with migration logic pertaining to files + + Scenario: I want to easily replace a string in a file using a literal expression + Given I have a file containing the string "123abc456" + When I use the literal "abc" to substitute "def" + Then the file should now contain the string "123def456" + + Scenario: I want to easily replace a string in a file using a regex expression + Given I have a file containing the string "123abc456" + When I use the regex "(?<=123).*(?=456)" to substitute "def" + Then the file should now contain the string "123def456" + + Scenario: I want to easily replace a substring of a string in a file using a regex expression + Given I have a file containing the string "123abc456" + When I use the regex "123.*456" to substitute "abc" with "def" + Then the file should now contain the string "123def456" + + Scenario: I want to easily test if a string is present in a file using a regex expression + Given I have a file containing the string "123abc456" + When I use the regex "123.*456" to search for matches in a file + Then the match result should be "true" + + Scenario: I want to easily be able to pull out specific substrings from a string using regex capture groups + Given I have a string containing "123abc456" + When I use the regex "123(.*)456" to retrieve capture groups + Then the size of the retrieved capture groups should be "1" + And the capture groups should include + | abc | + + Scenario: I want to easily be able to pull out specific strings from a file using regex capture groups + Given I have a file containing the string "123abc456" + When I use the regex "(123).*(456)" to retrieve capture groups + Then the size of the retrieved capture groups should be "2" + And the capture groups should include + | 123 | + | 456 | + + Scenario: I want to easily retrieve the type of indexing being used in a string + + Scenario: I want to easily indent a group of strings with spaces + + Scenario: I want to easily retrieve the contents of a file + + Scenario: I want to easily write to a file diff --git a/examples/example-migration-configuration/src/main/java/org/technologybrewery/baton/ReplaceRegexMatchUsingUtilsMigration.java b/examples/example-migration-configuration/src/main/java/org/technologybrewery/baton/ReplaceRegexMatchUsingUtilsMigration.java new file mode 100644 index 0000000..fbed013 --- /dev/null +++ b/examples/example-migration-configuration/src/main/java/org/technologybrewery/baton/ReplaceRegexMatchUsingUtilsMigration.java @@ -0,0 +1,41 @@ +package org.technologybrewery.baton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.technologybrewery.baton.util.FileUtils; + +import java.io.File; + +public class ReplaceRegexMatchUsingUtilsMigration extends AbstractMigration { + private static final Logger logger = LoggerFactory.getLogger(ReplaceRegexMatchUsingUtilsMigration.class); + private static final String JAVA_FILE_REGEX = "JavaFunctionToBeMigrated"; + private static final String NEW_JAVA_FILE_NAME = "MigratedJavaFunction"; + @Override + protected boolean shouldExecuteOnFile(File file) { + boolean shouldExecuteMigration = false; + try { + logger.info("Detecting migrations for {} to {}", JAVA_FILE_REGEX, NEW_JAVA_FILE_NAME); + shouldExecuteMigration = FileUtils.hasRegExMatch(JAVA_FILE_REGEX, file); + } catch (Exception e) { + logger.error("Caught exception checking Java files for migration legibility", e); + } + + return shouldExecuteMigration; + } + + @Override + protected boolean performMigration(File file) { + boolean migrationPerformed = false; + try { + migrationPerformed = FileUtils.replaceInFile(file, JAVA_FILE_REGEX, NEW_JAVA_FILE_NAME); + logger.info("Example migration for {} to {} performed successfully", JAVA_FILE_REGEX, NEW_JAVA_FILE_NAME); + + FileUtils.replaceInFile(file, NEW_JAVA_FILE_NAME, JAVA_FILE_REGEX); + logger.info("Reversed {} Migration to {} for demonstration purposes", JAVA_FILE_REGEX, NEW_JAVA_FILE_NAME); + } catch (Exception e) { + logger.error("Caught exception migrating Java function name"); + } + + return migrationPerformed; + } +} diff --git a/examples/example-migration-configuration/src/main/resources/migrations.json b/examples/example-migration-configuration/src/main/resources/migrations.json index 64a4d0e..33986e8 100644 --- a/examples/example-migration-configuration/src/main/resources/migrations.json +++ b/examples/example-migration-configuration/src/main/resources/migrations.json @@ -64,5 +64,20 @@ ] } ] + }, + { + "group": "group-4", + "type": "ordered", + "migrations": [ + { + "name": "test-migration-f", + "implementation": "org.technologybrewery.baton.ReplaceRegexMatchUsingUtilsMigration", + "fileSets": [ + { + "includes": ["**/*.java"] + } + ] + } + ] } ] \ No newline at end of file diff --git a/examples/example-projects/file-change-with-utils-example/pom.xml b/examples/example-projects/file-change-with-utils-example/pom.xml new file mode 100644 index 0000000..65e172c --- /dev/null +++ b/examples/example-projects/file-change-with-utils-example/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + org.technologybrewery.baton + example-projects + 1.1.0-SNAPSHOT + + + file-change-with-utils-example + + + + + ${project.groupId} + baton-maven-plugin + + + + + \ No newline at end of file diff --git a/examples/example-projects/file-change-with-utils-example/src/main/java/JavaFileToBeMigrated.java b/examples/example-projects/file-change-with-utils-example/src/main/java/JavaFileToBeMigrated.java new file mode 100644 index 0000000..4059d97 --- /dev/null +++ b/examples/example-projects/file-change-with-utils-example/src/main/java/JavaFileToBeMigrated.java @@ -0,0 +1,14 @@ +/** + * Just here to show that it gets picked up for migration-b, but not migration-a. + */ +public class JavaFileToBeMigrated { + + public JavaFileToBeMigrated() { + + } + + private boolean JavaFunctionToBeMigrated() { + return true; + } + +} \ No newline at end of file diff --git a/examples/example-projects/pom.xml b/examples/example-projects/pom.xml index 2c85df9..bae867d 100644 --- a/examples/example-projects/pom.xml +++ b/examples/example-projects/pom.xml @@ -17,6 +17,7 @@ simple-migration-example ignore-migration-example custom-backup-location-example + file-change-with-utils-example