In this post I’m using the Maven Enforcer plugin to break the build when certain files don’t follow the expected naming convention. It’s always a good idea to take the time and implement these checks inside the build pipeline. The alternative is hoping that code reviewers will spot the problems, which is a manual, tedious and error prone approach. Automate all the things!

The use case is that we want our FlyWay database migrations (sql scripts) to be prefixed with a timestamp, in order to avoid collisions when developers work on different tickets that require a database change. You can read more about that topic here. The idea is that if two developers are independently changing the database at the same time, they’ll both create a migration called v4.sql (for example). The two migrations are completely unrelated so one of them will have a git conflict. If they follow a timestamp filename convention instead, it reduces the chance for such issues (e.g. v20170417181600.sql).

After a bit of googling, I discovered this stackoverflow question which suggests using the Maven Enforcer plugin. The plugin introduces itself as The Loving Iron Fist of Maven, which got me sold already.

The plugin has various standard rules you can incorporate in your setup. The most extensible one is the evaluateBeanshell rule. It allows you to write custom code inside your pom in a scripting language called BeanShell (never heard of it, but it is like Java). As long as the code is an expression that evaluates into a boolean, it can consist of any amount of code (power, unlimited power).

This is how the whole thing looks like (it’s inside build/plugins):

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <version>1.4.1</version>
    <executions>
        <execution>
            <id>migration-filename-convention</id>
            <goals>
                <goal>enforce</goal>
            </goals>
            <phase>validate</phase>
            <configuration>
                <rules>
                    <evaluateBeanshell>
                        <condition>
                        List filenames = org.codehaus.plexus.util.FileUtils.getFileNames(
                            new File("src"),
                            "**/*.sql",
                            null,
                            false);

                        for (Iterator it = filenames.iterator(); it.hasNext();) {
                            String file = it.next();
                            print("Found SQL file: " + file);
                            passesValidation = java.util.regex.Pattern.matches("^.+[\\/\\\\]V[0-9]{4}([0-1][0-9])([0-3][0-9])[0-9]{6}__BDV.sql$", file);
                            if (passesValidation) {
                                print("Filename passes validation");
                                it.remove();
                            } else {
                                print("Did not pass validation");
                            };
                        };

                        filenames.isEmpty()</condition>
                    </evaluateBeanshell>
                </rules>
                <fail>true</fail>
            </configuration>
        </execution>
    </executions>
</plugin>

It configures an execution of this plugin named migration-filename-convention. It runs the enforce goal of the plugin and it hooks into the validate phase of the lifecycle. This means that you can just run mvn validate to run this custom check.

The code of the rule is contained inside the condition element:

List filenames = org.codehaus.plexus.util.FileUtils.getFileNames(
	new File("src"),
	"**/*.sql",
	null,
	false);

for (Iterator it = filenames.iterator(); it.hasNext();) {
	String file = it.next();
	print("Found SQL file: " + file);
	passesValidation = java.util.regex.Pattern.matches("^.+[\\/\\\\]V[0-9]{4}([0-1][0-9])([0-3][0-9])[0-9]{6}__BDV.sql$", file);
	if (passesValidation) {
		print("Filename passes validation");
		it.remove();
	} else {
		print("Did not pass validation");
	};
};

filenames.isEmpty()

This is practically Java code with some differences:

  • you don't have to declare the type of the variables (e.g. passesValidation is clearly a boolean)
  • you can use print for writing text
  • the most important part: this is supposed to be still a single boolean expression to be evaluated as true or false. That's why you need all the extra semicolons after the for and if statements. Notice that the last line filenames.isEmpty() which is in the end controls if the build passes or not.

The logic is simple:

  • Get a list of all SQL files inside the src folder
  • Iterate over the found files
  • Test the filename against a regular expression. If it matches, remove it from the list.
  • This leaves the list containing only invalid filenames. Therefore the build should be green when the list is empty after the for loop.

The print statements are useful for seeing what the rule is doing. This is an example of the plugin in action:

[INFO]
[INFO] --- maven-enforcer-plugin:1.4.1:enforce (migration-filename-convention) @ inventory-microservice ---
Found SQL file: main\resources\database\V20170803113900__BDV.sql
Filename passes validation
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

The other alternative is to implement a full blown custom rule in Java. While that is more structured than adding scripting code inside the pom, it is over-engineering in my mind for what I want. It requires setting up a new project, so that the custom rule can be added as a dependency to the Maven Enforcer plugin’s dependencies. That means setting up a git repository, publishing it somewhere in Nexus, having a CI/CD pipeline, the works. You could argue that then you have a custom rule that you can easily share across projects. Or even make a rule that comes with the desired naming convention built-in.

I see many developers who try to make things generic and abstract way too soon. In a lot of these cases, I try to follow You ain’t gonna need it. In this particular case, I would first implement the naming convention with the beanshell script, as shown in this post. This is enough to solve my actual problem (having consistent filenames). I would only invest in a separate custom rule if the beanshell approach starts to hurt too much for whatever reason.