Writing your own maven plugin

As some of you may know, I’m currently working on the WCMS project at the Australian Broadcasting Corporation. This is a large, multi-year project to replace the old web content management system with a brand new (Java) CoreMedia system which will allow the ABC to grow it’s online presence even further.

As with most java based systems, we use maven to support building the project. And we use many of the plugins that are out there to do specific tasks, for instance the maven-config-processor-plugin to create the various configurations for each of the target platforms, but also the maven-shade-plugin to change some .class files in specific jars.

One of these days I needed a plugin which was able to combine a few javascript files into one big one. So I googled for a few minutes to see if there was any plugin that could do this trick. Sadly enough, I was out of luck. Every promising link resulted in using the maven-ant-plugin and writing an ant task. I’m not a fan of escaping maven and falling back on ant, so I decided to look into writing my own plugin, I mean, how hard could joining files be?

To create a maven plugin, you will need the maven-plugin-api, which you configure in the dependencies part of the pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<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>net.sirious.maven.examples.filejoiner</groupId>
  <artifactId>maven-filejoiner-plugin</artifactId>
  <packaging>maven-plugin</packaging>
  <version>1.0</version>
  <name>maven-filejoiner-plugin</name>
  <url>http://sirious.net</url>
 
  <dependencies>
    <dependency>
      <groupId>org.apache.maven</groupId>
      <artifactId>maven-plugin-api</artifactId>
      <version>2.0</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
       <artifactId>maven-plugin-plugin</artifactId>
       <version>2.3</version>
       <configuration>
         <goalPrefix>filejoiner</goalPrefix>
       </configuration>
     </plugin>
     <plugin>
       <groupId>org.apache.maven.plugins</groupId>
       <artifactId>maven-compiler-plugin</artifactId>
       <version>2.3.1</version>
         <configuration>
          <source>1.5</source>
          <target>1.5</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Note that instead of packaging this as a jar, it’s packaged as a maven-plugin. Also, because we’ll be using annotations in the actual java source, we’ll set the source and target to use java 5.

Next, we’ll have to write some code that actually knows how to join an array of Files to another File. Thankfully, the AbstractMojo already has a everything on board to create a plugin. Just create a class that extends the AbstractMojo and implement the execute() method. Note that the class is annotated in the javadoc using the @goal and @phase annotations. These define the goal (in this case, it will be filejoiner:join) and the phase for the plugin (I chose prepare-package as I needed to join the files only when we’re constructing the final jar)

As we’re joining a list of files to a single file, we need a targetFile and an array of sourceFiles. We mark these in the javadoc as @parameter and make sure they are required by marking them @required. When using our plugin in another maven project, it will now know the names of the parameters as well as inform you that you’ve forgotten to provide them. The rest of the source is pretty¬†straight forwarded, we open a file for writing/appending and we’ll read the array of source files and append them to the target file, we throw in some getters and setters and we’re done. I could have made the encoding configurable too, but I didn’t because I always use UTF-8 and I strongly think everybody should ūüėČ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package net.sirious.maven.examples.filejoiner;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import java.io.*;
import java.util.List;
import java.util.Scanner;
/**
 * @author Taco Kemna
 *
 * @goal join
 * @phase prepare-package
 */
public class FileJoinerMojo extends AbstractMojo {
  private static final String DELIMITER = System.getProperty("line.separator");
  private static final String CHARSET = "UTF-8";
 /**
  * Location of the target file
  * @parameter
  *    property="targetFile"
  *    alias="targetFile"
  *
  * @required
  */
  private File targetFile;
 /**
  * source files
  * @parameter
  *    property="sourceFiles"
  *    alias="sourceFiles"
  *
  * @required
  */
  private File[] sourceFiles;
  public void execute() throws MojoExecutionException {
    File target = targetFile;
    StringBuilder output = new StringBuilder();
    if (target.exists()) {
      output.append(readFile(target));
      getLog().info("loaded targetFile " + target.getAbsolutePath());
    } else {
      // make sure the folder we'll be writing to exists...
      if (!target.getParentFile().exists()) {
        boolean created = target.getParentFile().mkdirs();
        if (created) {
          getLog().info("created new folders " + target.getParentFile().getAbsolutePath());
        } else {
          getLog().error("unable to create target folders, exitting.");
          return;
        }
      }
    }
    // append all the sourcefiles
    for (File source : sourceFiles) {
      if (source.exists()) {
        getLog().info("appending source " + source.getAbsolutePath());
        output.append("\n").append(readFile(source));
      } else {
        getLog().error("file did not exist: " + source.getAbsolutePath());
      }
    }
    // and write the output
    write(target, output);
  }
 /**
  * Writes the String to a file
  * @param target File target file to write to
  * @param output StringBuilder containing the data to be written
  */
  private void write(File target, StringBuilder output) {
    // write the entire buffer
    Writer out = null;
    try {
      out = new OutputStreamWriter(new FileOutputStream(target), CHARSET);
      out.write(output.toString());
      out.close();
    } catch (UnsupportedEncodingException e) {
      getLog().error("unsupported encoding: " + CHARSET, e);
    } catch (FileNotFoundException e) {
      getLog().error("file not found: " + targetFile, e);
    } catch (IOException e) {
      getLog().error("i/o exception", e);
    }
  }
 /**
  * Reads a file into a String
  * @param file File
  * @return String
  */
  private String readFile(File file) {
    StringBuilder sb = new StringBuilder();
    Scanner scanner = null;
    try {
      scanner = new Scanner(file, CHARSET);
      scanner.useDelimiter(DELIMITER);
    } catch (FileNotFoundException e) {
      getLog().error("file not found while trying to read file " + file.getAbsolutePath(), e);
      return "";
    }
    while (scanner.hasNextLine()) {
      sb.append(scanner.nextLine()).append(DELIMITER);
    }
    scanner.close();
    return sb.toString();
  }
 /**
  * @return File[]
  */
  public File[] getSourceFiles() {
    return sourceFiles;
  }
 /**
  * @param sourceFiles File[] to set.
  */
  public void setSourceFiles(File[] sourceFiles) {
    this.sourceFiles = sourceFiles;
  }
 /**
  * @return File
  */
  public File getTargetFile() {
    return targetFile;
  }
 /**
  * @param targetFile File to set.
  */
  public void setTargetFile(File targetFile) {
    this.targetFile = targetFile;
  }
}

And that is all that there is to it. How maven knows to use this class? No magic there, just because you annotated it with @goal, it will be picked up by maven. So you can put multiple goals in one plugin, nicely¬†separated¬†into different classes. Of course, to deploy this plugin to your repository, you will need to add a ‘distributionManagement’ section to your pom.xml, but if you just have a local repository, running ‘mvn install’ will do.

Now, to use our plugin in another project, we’ll need to configure it as a plugin. We already decided that the execution phase for this plugin was to be ‘prepare-package’, so, we configure the ‘join’ task during that phase and we configure some sourceFiles and a targetFile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
  <groupId>net.sirious.maven.examples.filejoiner</groupId>
  <artifactId>maven-filejoiner-plugin</artifactId>
  <version>1.0</version>
  <executions>
    <execution>
      <phase>prepare-package</phase>
      <goals>
        <goal>join</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <targetFile>${basedir}/target/combined.txt</targetFile>
    <sourceFiles>
      <sourceFile>${basedir}/src/main/resources/1.txt</sourceFile>
      <sourceFile>${basedir}/src/main/resources/2.txt</sourceFile>
    </sourceFiles>
  </configuration>
</plugin>

So, now, whenever we run the ‘prepare-package’ or during any later phase, our plugin will take 1.txt and 2.txt and append them to a new or existing combined.txt. Easy does it. I’m pretty sure that writing the plugin, distributing and configuring it in our project actually took less time than this blog post ūüôā

More reading: