More‎ > ‎

How to Write a Custom Plugin for Maven to Count Lines of Code?

BY MARKUS SPRUNCK


This article describes how to implement a simple custom maven plugin for counting total lines of code - creates a xml file as output with the numbers of lines by file type. You may download the Eclipse project with the source code, see section download below.

How to Create a Minimal Maven Plugin?

To create your first empty maven plugin project you just have to create a new maven project in Eclipse with maven-archetype-mojo (see Figure 1). 

 Figure 1: Select maven-archetype-mojo

Then you input the Group-Id and Artifact-Id (see Figure 2). The Artifact-Id will be the later project name.

Figure 2: Enter your Group Id and Artefact Id
 
The generated maven plugin project contains a class named MyMojo which is derived from the class AbstractMojo and has a method called execute(). This execute() method will later be called from maven and is the entry point for your maven plugin.

 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
package com.sprunck.sample.tlocc;
 
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
 
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
 
/**
 * Goal which touches a timestamp file.
 *
 * @goal touch
 * 
 * @phase process-sources
 */
 
public class MyMojo
    extends AbstractMojo
{
 
    /**
     * Location of the file.
     * @parameter expression="${project.build.directory}"
     * @required
     */
    private File outputDirectory;
 
    public void execute()
        throws MojoExecutionException
    {
        File f = outputDirectory;
 
        if ( !f.exists() )
        {
            f.mkdirs();
        }
 
        File touch = new File( f, "touch.txt" );
 
        FileWriter w = null;
        try
        {
            w = new FileWriter( touch );
 
            w.write( "touch.txt" );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Error creating file " + touch, e );
        }
        finally
        {
            if ( w != null )
            {
                try
                {
                    w.close();
                }
                catch ( IOException e )
                {
                    // ignore
                }
            }
        }
    }
}

The maven API uses JavaDoc comments to identify the attributes to be updated. For instance the comment @parameter expression="${project.build.directory}" is used to identify the attribute for the defined output directory.

Sample Code - Maven Plugin for Counting Lines of Code

The following class (see File #01) has been developed on the basis of the MyMojo class mentioned in the last section. The sample code compiles with JDK 1.6 and has been developed with Eclipse Indigo and M2E plugin.

As you can see it is a very simple implementation of a total lines of code counter. The implementation could be improved in many ways, e.g. format the source code before counting, output of separated production and test code counts, send the xml result file with JMS to a reporting system also the error handling could be improved.  
File LinesOfCodeCounterMojo.java

  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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
package com.sprunck.sample.tlocc;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
 
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
 
import com.thoughtworks.xstream.XStream;
 
/**
 * Goal which counts the total lines of code 
 * 
 * @goal tlocc
 * 
 * @phase test
 */
public class LinesOfCodeCounterMojo extends AbstractMojo {
 
    /** For each file type (extension) one entry in the map */
    private final Map< String, CountResult > fileTypes = new HashMap< String, CountResult >(100);
 
    /** List with all files to be counted */
    private final List< String > files = new ArrayList< String >(10000);
 
    /** Code lines of current file */
    private int currentFileEmptyLines = 0;
 
    /** Total code lines of current file */
    private int currentFileTotalLines = 0;
 
    /**
     * Location of the file.
     * 
     * @parameter expression="${project.build.directory}"
     * @required
     */
    private final File outputDirectory = new File("");
 
    /**
     * Project's source directory as specified in the POM.
     * 
     * @parameter expression="${project.build.sourceDirectory}"
     * @readonly
     * @required
     */
    private final File sourceDirectory = new File("");
 
    /**
     * Project's source directory for test code as specified in the POM.
     * 
     * @parameter expression="${project.build.testSourceDirectory}"
     * @readonly
     * @required
     */
    private final File testSourceDirectory = new File("");
 
    /**
     * Encoding of source files
     * 
     * @parameter expression="${encoding}"
     *            default-value="${project.build.sourceEncoding}"
     */
    private final String encoding = "UTF-8";
 
    @Override
    public void execute() throws MojoExecutionException {
 
        if (!ensureTargetDirectoryExists()) {
            getLog().error("Could not create target directory");
            return;
        }
 
        if (!sourceDirectory.exists()) {
            getLog().error("Source directory \"" + sourceDirectory + "\" is not valid.");
            return;
        }
 
        fillListWithAllFilesRecursiveTask(sourceDirectory, files);
        fillListWithAllFilesRecursiveTask(testSourceDirectory, files);
 
        for (final String filePath : files) {
            resetCurrentCounts();
            countCurrentFile(filePath);
            writeCurrentResultsToMavenLogger(filePath);
            storeCurrentResults(filePath);
        }
 
        writeOutputFileFromStoredResults();
    }
 
    private boolean ensureTargetDirectoryExists() {
        if (outputDirectory.exists()) {
            return true;
        }
        return outputDirectory.mkdirs();
    }
 
    public void resetCurrentCounts() {
        currentFileEmptyLines = 0;
        currentFileTotalLines = 0;
    }
 
    public void countCurrentFile(final String filePath) {
        try {
            final Scanner scan = new Scanner(new File(filePath), encoding);
            String line = scan.nextLine();
            while (scan.hasNext()) {
                currentFileTotalLines++;
                if (line.trim().isEmpty()) {
                    currentFileEmptyLines++;
                }
                line = scan.nextLine();
            }
        } catch (final IOException e) {
            getLog().error(e.getMessage());
        }
    }
 
    public void writeCurrentResultsToMavenLogger(final String filePath) {
        final StringBuffer message = new StringBuffer(100);
        message.append(currentFileEmptyLines).append('\t');
        message.append(currentFileTotalLines).append('\t');
        message.append(filePath);
        getLog().info(message);
    }
 
    public void storeCurrentResults(final String filePath) {
        final String extension = getExtension(filePath);
        if (fileTypes.containsKey(extension)) {
            fileTypes.get(extension).addEmpty(currentFileEmptyLines);
            fileTypes.get(extension).addTotal(currentFileTotalLines);
            fileTypes.get(extension).incrementFiles();
        } else {
            final CountResult item = new CountResult();
            item.addEmpty(currentFileEmptyLines);
            item.addTotal(currentFileTotalLines);
            item.incrementFiles();
            fileTypes.put(extension, item);
        }
    }
 
    private void writeOutputFileFromStoredResults() {
        OutputStreamWriter out = null;
        try {
            final StringBuffer path = new StringBuffer();
            path.append(outputDirectory);
            path.append(System.getProperty("file.separator"));
            path.append("tlocc-result.xml");
 
            final FileOutputStream fos = new FileOutputStream(path.toString());
            out = new OutputStreamWriter(fos, encoding);
 
            final XStream xstream = new XStream();
            xstream.alias("data", CountResult.class);
            out.write(xstream.toXML(fileTypes));
 
        } catch (final IOException e) {
            getLog().error(e.getMessage());
        } finally {
            if (null != out) {
                try {
                    out.close();
                } catch (final IOException e) {
                    getLog().error(e.getMessage());
                }
            }
        }
    }
 
    public static void fillListWithAllFilesRecursiveTask(final File root, final List< String > files) {
        if (root.isFile()) {
            files.add(root.getPath());
            return;
        }
        for (final File file : root.listFiles()) {
            if (file.isDirectory()) {
                fillListWithAllFilesRecursiveTask(file, files);
            } else {
                files.add(file.getPath());
            }
        }
    }
 
    public static String getExtension(final String filePath) {
        final int dotPos = filePath.lastIndexOf(".");
        if (-1 == dotPos) {
            return "undefined";
        } else {
            return filePath.substring(dotPos);
        }
    }
 
}

Please have a look at the encoding attribute. Correct encoding will be needed to run maven on different platforms, e.g. UNIX, Windows. To compile an run the LinesOfCodeCounterMojo class a second class CountResult (see File #02) will be needed. This class stores just the statistic for later output.

File CountResult.java

 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
package com.sprunck.sample.tlocc;
 
/** For each counted file type one Item object stores the result*/
public class CountResult {
 
    /** Number of total lines (with empty lines) */
    private long total = 0;
 
    /** Number of empty lines */
    private long empty = 0;
 
    /** Number of files*/
    private long files = 0;
 
    public long getTotal() {
        return total;
    }
 
    public void addTotal(final long lines) {
        this.total += lines;
    }
 
    public long getEmpty() {
        return empty;
    }
 
    public void addEmpty(final long lines) {
        this.empty += lines;
    }
 
    public long getFiles() {
        return files;
    }
 
    public void incrementFiles() {
        this.files += 1;
    }
}  

You should now insert the following lines into your setting.xml file:

1
2
3
<plugingroups>
      <plugingroup>com.sw_engineering_candies</plugingroup>
</plugingroups>

and insert the following code in your pom.xml file:

1
2
3
<properties>
    <project.build.sourceencoding>UTF-8</project.build.sourceencoding>
</properties>
 
Now you can run the sample code (File #01 and #02) with the following maven goal: 

1
clean install tlocc:tlocc 

and you should see something like this:

 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
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building tlocc Maven Mojo 1.0
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ tlocc ---
[INFO] Deleting C:\Projects\.eclipse3.7.rcp\tlocc\target
[INFO] 
[INFO] --- maven-plugin-plugin:2.9:descriptor (default-descriptor) @ tlocc ---
[INFO] Using 'UTF-8' encoding to read mojo metadata.
[INFO] Applying mojo extractor for language: java
[INFO] Mojo extractor for language: java found 1 mojo descriptors.
[INFO] Applying mojo extractor for language: bsh
[INFO] Mojo extractor for language: bsh found 0 mojo descriptors.
[INFO] 
[INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ tlocc ---
[debug] execute contextualize
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Projects\.eclipse3.7.rcp\tlocc\src\main\resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ tlocc ---
[INFO] Compiling 2 source files to C:\Projects\.eclipse3.7.rcp\tlocc\target\classes
[INFO] 
[INFO] --- maven-resources-plugin:2.5:testResources (default-testResources) @ tlocc ---
[debug] execute contextualize
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Projects\.eclipse3.7.rcp\tlocc\src\test\resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ tlocc ---
[INFO] Not compiling test sources
[INFO] 
[INFO] --- maven-surefire-plugin:2.10:test (default-test) @ tlocc ---
[INFO] Tests are skipped.
[INFO] 
[INFO] --- maven-jar-plugin:2.3.1:jar (default-jar) @ tlocc ---
[INFO] Building jar: C:\Projects\.eclipse3.7.rcp\tlocc\target\tlocc-1.0.jar
[INFO] 
[INFO] --- maven-plugin-plugin:2.9:addPluginArtifactMetadata (default-addPluginArtifactMetadata) @ tlocc ---
[INFO] 
[INFO] --- maven-install-plugin:2.3.1:install (default-install) @ tlocc ---
[INFO] Installing C:\Projects\.eclipse3.7.rcp\tlocc\target\tlocc-1.0.jar to C:\Users\Markus\.m2\repository\com\sprunck\sample\tlocc\1.0\tlocc-1.0.jar
[INFO] Installing C:\Projects\.eclipse3.7.rcp\tlocc\pom.xml to C:\Users\Markus\.m2\repository\com\sprunck\sample\tlocc\1.0\tlocc-1.0.pom
[INFO] 
[INFO] --- tlocc:1.0:tlocc (default-cli) @ tlocc ---
[INFO] 11 38 C:\Projects\.eclipse3.7.rcp\tlocc\src\main\java\com\sprunck\sample\tlocc\CountResult.java
[INFO] 30 200 C:\Projects\.eclipse3.7.rcp\tlocc\src\main\java\com\sprunck\sample\tlocc\LinesOfCodeCounterMojo.java
[INFO] 1 2 C:\Projects\.eclipse3.7.rcp\tlocc\src\main\java\com\sprunck\sample\tlocc\testfiles\foo.properties
[INFO] 4 13 C:\Projects\.eclipse3.7.rcp\tlocc\src\main\java\com\sprunck\sample\tlocc\testfiles\gee.xml
[INFO] 0 7 C:\Projects\.eclipse3.7.rcp\tlocc\src\test\java\com\sprunck\sample\tlocc\testfiles\bee.xml
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.722s
[INFO] Finished at: Sun Jul 29 11:56:59 CEST 2012
[INFO] Final Memory: 12M/30M
[INFO] ------------------------------------------------------------------------

In the target directory the following result file will be created:

 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
<map>
  <entry>
    <string>.properties</string>
    <data>
      <total>2</total>
      <empty>1</empty>
      <files>1</files>
    </data>
  </entry>
  <entry>
    <string>.xml</string>
    <data>
      <total>20</total>
      <empty>4</empty>
      <files>2</files>
    </data>
  </entry>
  <entry>
    <string>.java</string>
    <data>
      <total>238</total>
      <empty>41</empty>
      <files>2</files>
    </data>
  </entry>
</map>

Recommendations

If you are not familiar with maven plugins you may also read Maven - Guide to Developing Java Plugins and Maven - Mojo API Specification for more details. 

Find Code on GitHub

Sponsored Link