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