There is a Mojo in My Dojo (How to write a Maven plugin)

Standard

I have been up to my armpits involved using Maven at work.  For good number of developers I will hear, “So what.”  The difference is that I normally work in environments where I do not have access to the Internet directly.  So when I say I have been using Maven a lot, it means something.

Dependency Hell

To be fair, I have been using Maven casually in my examples.  I have found it to be more convenient to get downloads of dependencies and avoid “dependency hell.”  The situation where I have to download a library for a library that i am using.  For example, one has to download Hamcrest to use JUnit.  At home, put in the dependency for JUnit and Maven downloads Hamcrest for me because it is a dependency of JUnit.  If there was a dependency of Hamcrest, Maven would download that too.  When I am at work, I need to research what dependencies JUnit has and then research what dependencies the dependencies have.  I have avoided using libraries because of this very situation.

Situations Change

The change is because I am using Spring Roo at work.  Roo uses Maven to manage Spring dependencies that it needs to incorporate.  Because of this change, I set up a Nexus server on the development network and started the process of bringing over the dependencies from the Internet to the development network.  This got me learning about Maven.

What I Learned about Maven

After reading two books, Introducing Maven and Maven Build Customization, I got a pretty good idea about Maven and how to create the subject of this post.  I can go on and on about what I learned but I will keep it focused to what is needed to learn about Maven plugins. I do assume one has seen a pom file and run a few Maven builds from now on in the post.  If one has not, purchase the books I read or go to http://maven.apache.org first.

Maven is Plugin Rich

Maven is based on a plugin architecture.  Anything that does something in Maven is a plugin.  That goes from core functionality like compiling to creating sites.  As one can imagine, every plugin has certain things in common.

Maven is Package, Lifecycle, Phase and Goal Oriented

Maven is known for building something into a packaged item of some sort, for example a jar file.  That is obvious, that is one of the first lines of a pom file.  What may not be known is that there is a series of “phases” or “lifecycle” that happen to accomplish building the package (see what I did there).  In fact, one of those phases is named “package.”  The list of default phases in a lifecycle are as follows:

  1. validate
  2. generate-sources
  3. process-sources
  4. generate-resources
  5. process-resources
  6. compile
  7. process-classes
  8. generate-test-sources
  9. process-test-sources
  10. generate-test-resources
  11. process-test-resources
  12. test-compile
  13. process-test-classes
  14. test
  15. prepare-package
  16. package
  17. pre-integration-test
  18. integration-test
  19. post-integration-test
  20. verify
  21. install
  22. deploy

There is a lot of stuff going on in a Maven build!  All of that is being run by some sort of plugin.  Every plugin is made of goals which can be set to run at a certain phase of the lifecycle.  For example, the maven-jar-plugin’s jar goal is set to run in the package phase.

The Making of a Plugin

Now that one has a more in-depth knowledge of what is going on in a build, it is time to explain what is needed to create a Maven plugin.

Plugins Are Full of Mojos

What is a mojo?  Mojo is short for Maven plain Old Java Objects.  It is the smallest unit of a plugin Maven recognizes.  All plugins are made of mojos.  Each mojo is associated to a goal.  So for a plugin to have multiple goals, it needs multiple mojos.  The example I will show only has one mojo sadly but the example will also show best practices in testing a plugin.

Best Practices are the Only Practices Allowed

See what I did there to tie in with the Dojo deal in the title?  There is naming convention, unit testing and integration testing involved with writing plugins if one is inclined.  The naming convention is the most important so

  1. You don’t bust an Apache trademark
  2. Others know that one made a plugin.

 What is in a Name?

The naming convention for Apache’s plugins are maven-<title>-plugin.  For example, the jar plugin is named maven-jar-plugin.  For everyone else, the naming convention is <title>-maven-plugin.  For example, the example I created is named reminder-maven-plugin.  Another example that used when making this post is Spring Boot‘s plugin and it is named spring-boot-maven-plugin.  The source code to Spring Boot is here.  I forked it so I could peruse and abuse the code.  My fork can be found here.  If one wants to abuse it together, please fork my copy and send me a pull request when your particular piece of abuse is done.  Anyway if one uses Apache’s naming convention, it is a trademark infringement.  You have been warned.

Unit Testing

Automated unit and integration testing is important too.  Unit testing follows a little different directory pattern than normal unit testing so hang on.

The directory structure when doing a unit test of a plugin is

unit_test_plugin

Notice that all of the test directories are organized under the test directory.  What one is making is a little version of a project that will be using the plugin.  Under the test resources directory is a unit directory followed by the name of the unit in the child directory.  The goal is to test a single mojo at a time.  Since my example only has one mojo, I only set up one test.  There are other differences than the directory setup but that will be covered in the example section.

Integration Testing

I found that this testing will teach one the most about one’s particular plugin and how it works.  The goal is to test a certain situation as if it was part of an actual project build.  When I mean actual project build, I mean that there is even a temporary repository just for the integration build.  After reading about how to set up the tests, I borrowed heavily from spring-boot-maven-plugin’s integration test setup and mini pom files.  OK, I copied some of the files over to my example code.  Just informing one that Spring Boot did it right.  Just be safe a clone read only or fork their code just to be safe.  The directory structure is displayed below.

it_test_plugin

The integration tests are located not under the test directory but directly underneath the src directory in the it directory.  I could have done more integration tests but one is good enough for now.

Example

The example plugin was inspired by the fact that I am absent minded and need to be reminded of everything I do.  I thought of creating a wash-the-dogs-reminder-maven-plugin but I decided on a plain reminder-maven-plugin because then I could use it to remind me of anything I needed to do.

Pom File

 

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.darylmathison</groupId>
    <artifactId>reminder-maven-plugin</artifactId>
    <packaging>maven-plugin</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>reminder-maven-plugin Maven Mojo</name>
    <url>http://maven.apache.org</url>

    <properties>
        <mavenVersion>3.2.1</mavenVersion>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Maven dependencies -->
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>${mavenVersion}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-core</artifactId>
            <version>${mavenVersion}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-compat</artifactId>
            <version>3.2.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-testing</groupId>
            <artifactId>maven-plugin-testing-harness</artifactId>
            <version>3.1.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-plugin-plugin</artifactId>
                    <version>3.2</version>
                    <executions>
                        <execution>
                            <id>mojo-descriptor</id>
                            <goals>
                                <goal>descriptor</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
    <profiles>
        <profile>
            <id>run-its</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-invoker-plugin</artifactId>
                        <version>1.7</version>
                        <configuration>
                            <debug>true</debug>
                            <cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
                            <cloneClean>true</cloneClean>
                            <pomIncludes>
                                <pomInclude>*/pom.xml</pomInclude>
                            </pomIncludes>
                            <addTestClassPath>true</addTestClassPath>
                            <postBuildHookScript>verify</postBuildHookScript>
                            <localRepositoryPath>${project.build.directory}/local-repo</localRepositoryPath>
                            <settingsFile>src/it/settings.xml</settingsFile>
                            <goals>
                                <goal>clean</goal>
                                <goal>compile</goal>
                                <goal>package</goal>
                            </goals>
                        </configuration>
                        <executions>
                            <execution>
                                <id>integration-test</id>
                                <goals>
                                    <goal>install</goal>
                                    <goal>run</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

 
As one can see, quite a few plugins and dependencies are needed to build one. There is one dependency of note here. This is the version of Junit. The version needs to be 3.8.1. This is because Maven extended the TestCase class to make it easier to unit test. That will be seen soon. Two plugins are of note, one is the maven-plugin-plugin and the other is the maven-invoker-plugin. The maven-plugin-plugin automates the process of creating a help goal for one’s plugin. The maven-invoker-plugin is used in the integration tests. Its function is to run Maven projects which is handy if one is running in a test pom.

ReminderMojo.java

 

package com.darylmathison;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

@Mojo(name = "remind",
        defaultPhase = LifecyclePhase.PACKAGE,
        requiresOnline = false, requiresProject = true,
        threadSafe = false)
public class ReminderMojo extends AbstractMojo {

    @Parameter(property = "basedir", required = true)
    protected File basedir;

    @Parameter(property = "message", required = true)
    protected String message;

    @Parameter(property = "numOfWeeks", defaultValue = "6", required = true)
    protected int numOfWeeks;

    public void execute() throws MojoExecutionException {

        File timestampFile = new File(basedir, "timestamp.txt");
        getLog().debug("basedir is " + basedir.getName());
        if(!timestampFile.exists()) {
            basedir.mkdirs();
            getLog().info(message);
            timestamp(timestampFile);
        } else {
            LocalDateTime date = readTimestamp(timestampFile);
            date.plus(numOfWeeks, ChronoUnit.WEEKS);
            if(date.isBefore(LocalDateTime.now())) {
                getLog().info(message);
                timestamp(timestampFile);
            }
        }
    }

    private void timestamp(File file) throws MojoExecutionException {
        try(FileWriter w = new FileWriter(file)) {
            LocalDateTime localDateTime = LocalDateTime.now();
            w.write(localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        } catch (IOException e) {
            throw new MojoExecutionException("Error creating file " + file, e);
        }
    }

    private LocalDateTime readTimestamp(File file) throws MojoExecutionException {
        try(FileReader r = new FileReader(file)) {
            char[] buffer = new char[1024];
            int len = r.read(buffer);
            LocalDateTime date = LocalDateTime.parse(String.valueOf(buffer, 0, len));
            return date;
        } catch(IOException ioe) {
            throw new MojoExecutionException("Error reading file " + file, ioe);
        }
    }
}

 
This is the only Mojo in the plugin and as one can find, it is very simple but shows some of the cool features the mojo api provides. The class annotation defines that the name of the goal is “remind” and that it is not thread safe. It also defines the default phase is the package phase. The last thing I will mention is that any member variable can become a parameter. This becomes a parameter for the plugin of a goal.

ReminderMojoTest

 

package com.darylmathison;

import org.apache.maven.plugin.testing.AbstractMojoTestCase;

import java.io.File;

/**
 * Created by Daryl on 3/31/2015.
 */
public class ReminderMojoTest extends AbstractMojoTestCase {

    @Override
    protected void setUp() throws Exception {
        super.setUp();
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    public void testJustMessage() throws Exception {
        File pom = getTestFile("src/test/resources/unit/reminder-mojo/pom.xml");
        assertNotNull(pom);
        assertTrue(pom.exists());
        ReminderMojo myMojo = (ReminderMojo) lookupMojo("remind", pom);
        assertNotNull(myMojo);
        myMojo.execute();
    }
}

 
Here is a basic unit test case of a mojo. The test class extends AbstractMojoTestCase to gain some functionality like getTestFile and lookupMojo. The following is the test pom file.

Unit Test Pom File

 

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.darylmathison.test</groupId>
    <artifactId>reminder-maven-plugin-test-reminder</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>reminder-maven-plugin Maven Mojo</name>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>com.darylmathison</groupId>
                <artifactId>reminder-maven-plugin</artifactId>
                <version>1.0-SNAPSHOT</version>
                <configuration>
                    <basedir>target/test-classes/unit/reminder-mojo</basedir>
                    <message>Wash the doggies</message>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

 
Just a mini version of the main pom file that defined the plugin.

Integration Test

This deserves its own section because it is really a separate Maven project within a Maven project. The main focus of this exercise is to see what the plugin will do and not anything else. The sample application is simple and just there for the Maven project to build. The other thing to note is that the pom file uses some filtering to match the groupId, artifactId, and version of the main plugin pom.

Pom File

 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.darylmathison.it</groupId>
    <artifactId>new-timestamp</artifactId>
    <version>0.0.1.BUILD-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>@project.groupId@</groupId>
                <artifactId>@project.artifactId@</artifactId>
                <version>@project.version@</version>
                <executions>
                    <execution>
                        <id>blah</id>
                        <goals>
                            <goal>remind</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <message>Wash the doggies</message>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

 

SampleApp

 

package java.test;

/**
 * Created by Daryl on 4/5/2015.
 */
public class SampleApp {
    public static void Main(String[] args) {
        System.out.println("out");
    }
}

 

Verify.groovy

 

System.out.println(basedir);
def file = new File(basedir, "timestamp.txt");
return file.exists();

 
The verify script is to make sure that the plugin does what it intended to do. It just checks for the existence of the timestamp.txt file because the plugin creates one when it cannot find a timestamp file. Maven checks for a true or false output of the verify script.

Conclusion

Wow! I covered a lot in this post. I went and gave an example of how to create a Maven plugin. I also showed how to test that plugin using best practices. I got the information between two books and an example of a real on going open source project. The example code is hosted on github here. This represents the first example in my new example home.

References

  • Introducing Maven
  • Maven Build Customization
  • http://maven.apache.org
  • Spring Boot
  • Advertisements

News Item: Moving Code Examples to GitHub

Standard

Since the start of this blog I have been hosting my example code on Google Code.  That has suddenly needed to change.  I received an email from Google that stated:

Hello,

Earlier today, Google announced we will be turning down Google Code Project Hosting. The service started in 2006 with the goal of providing a scalable and reliable way of hosting open source projects. Since that time, millions of people have contributed to open source projects hosted on the site.

Later on in the letter it states:

We will be shutting down Google Code over the coming months. Starting today, the site will no longer accept new projects, but will remain functionally unchanged until August 2015. After that, project data will be read-only. Early next year, the site will shut down, but project data will be available for download in an archive format.

That means a new home for my example code.

Thank you GitHub

I have already been hosting a few projects in GitHub but I wanted to separate my example code from my “personal stuff.”  Google offered an export service to GitHub and the convenience won me over.  My example code and personal projects are now on the same hosting site.

What Does that Mean For the Blog?

It means that the links that I have in current and past blog posts will have to be changed.  Currently Google will not shut off the links until next year so I have some time to change four years of examples to the new site.  As I change over the links, some may not work initially.  If one finds a bad link (one going to GitHub and not working) please let me know.and I will fix it.

Keep on Coding!

Daryl

RE:Does It Get Boring To Be A Programmer?

Standard

This is in response to the post written by Bozhidar Bozhanov, “Does It Get Boring To Be A Programmer?” hosted on Java Coding Geeks. It really reminds me of my career path and how I changed it.

I really programming when I was in elementary school on a TRS-80. Let us not get into the “I had to use tape” discussions please. In any case, the need program carried me on through collage and I graduated with a degree in Computer Engineering. Not only was I a programmer, I was an engineer. I had a license to get paid to do what I enjoy doing was how I saw it.

I am goal oriented so I knew if I wanted to get anywhere I was going to have to develop some goals. I was married by that time so one of my goals was to make sure my work did not get in the way of my marriage. Another goal I set was my career goal. It had to be general enough to fit a number of situations but specific enough to keep on path. The goals I had set kept me in the direction I was aiming but a snag hit along the way.

I had built a set routine in my life of going to work and coming home, going to work and coming home, etc. As the pattern was set, I did something that really threw everything off the tracks, I forgot my goals. Looking back at it, the routine that I had set was really about earning a paycheck and not about what really motivated me. I got my hours so I could pay the bills. That was about the extent of my reasons for going to work. This attitude led me to get bored, really bored. That boredom put me on the path of getting my hours to pay my bills. As one can see, it became a cycle.

The wheels started getting back on track after I got laid off. One of the contracts I was on ran out and no new money was coming so cutbacks were next. I had just blew a deadline so one of the logical choices was me. I really struggled personally as I was looking for work. My goals seemed a long way off now. I was in survival mode and nothing else. The goal of finding work to pay the bills was all consuming. I could never find a position that I was fully qualified for. I got two interviews in four months. That is not a good return on time. I did land a job that paid well under what I was earning in the last job but I had bills. I took the job and started the search again for a job that could pay all the bills.

I developed a habit of following up with every company that I had contact with. This was to keep me fresh in their minds. This habit did land me a job with a company that really has the customer and their employees first. I was getting the bills paid, goal accomplished and more, so why did my career still suck?

The reason my career still sucked was me. I started digging around in my memory to find out why I was fired up when I was just starting and not now. I remembered my career goal, “Solving interesting problems by technical means.” That started the ball rolling from sucky career to a fantastic career. My circumstances had not changed but I started to change.  I started to research different technologies, like Hazelcast, Spring and others.  I wrote my experiences down for everyone to share(blog). This research lead me to do a webinar presentation. I shared my knowledge with the team I was working with and discussed if we could incorporate it anywhere.  In my new job, I am using my knowledge of computer security to meet security requirements set by the customer.  All of this is fueled by my renewed commitment to my career goal.  This commitment has turned into a love of computers that I had back in elementary school with an old TRS-80.  My creativity has soared and with my new found love and professional experience I can design creative solutions to meet customer needs that are practical and sometimes even elegant.

So in direct response to the article, I have found that a programmer can be bored only if they let themselves be bored and loose track of what brought them into computers to begin with.  As for me and my career, I am a born-again programmer.