How to write a Custom Plugin for Maven?

In this article, we will learn about Maven which is a project management framework that provides a configurable approach for managing software projects. Maven covers all the necessary phases that happen right from project creation, building, documentation, reporting, installation and deployment. This article begins with the basics of Maven along with the concepts like Project Object Model (aka POM), the various life-cycles in Maven etc. Then it continues with using Maven for creating a project till installation of the project in a local repository. The latter part of the article provides details about creating custom Maven plugins that can be executed in stand-alone mode as well as part of some Maven life-cycle.

Maven Custom Plug-In (Example Code)

Maven Concepts

In this section, we will see the basics of Maven concepts like Project Object Model, the standard lifecycles available in Maven as well as the dependency scope that comes up while dealing with dependency with projects.

Project Object Model

Project information, its dependencies and the artifacts that can be built are described in POM which is essentially a configuration file in XML. POM basically contains instructions on what needs to be done during the various life-cycles that happen during a build. Note that the POM is not coupled to java projects, in fact the project can be written in any language like C, C# etc. For example, consider the sample minimal POM,

<project>

    <modelVersion>4.0.0</modelVersion>

    <name>adder</name>
    <url>http://www.javabeat.net/adder</url>
    <groupId>net.javabeat.adder</groupId>
    <artifactId>adder</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

Have a look at the above POM. It defines the information about the project and its dependant projects. We will look into more details about the various POM elements.

  • modelVersion – This defines the version of the POM.
  • name – The name of the project.
  • groupId – This identifies the group (or the organization) that created the project. This property usually takes the domain address which represents the group or the organization so that uniqueness is maintained across the groups.
  • artifactId – This identifies the artifact (output component) name that comes from a project. Artifacts can be anything like a JAR, WAR, EAR etc
  • version – This property identifies the version of the artifact. Usually the version takes the format as “majorVersion.minorVersion-qualifier”. For example, “1.1-alpha”, “1.2-beta”, “1.3-SNAPSHOT”.
    packaging – Represents the type of artifact, for example – jar, war, ear etc.

In the next section in the POM, we have included the dependencies of the project. Note that this section is optional. If there are no dependencies for a project (which is unlikely the case), this can be omitted. However, in this example case, we have declared a dependency to junit and it is expected that the project will contain some test resources. It is always a good idea to define the version of the artifact while defining a dependency, so that during runtime, we are not facing any issues because of version match. We have mentioned the scope as test, more details about this element will follow in the forthcoming section.

  • Log4j configurations

Dependency Scope

A project can depend on a number of dependent projects and Maven provides a way for defining the dependencies among projects through configuration. One can control the level of dependency through scope and the default being ‘compile’. The following scopes are available.

  • Compile – This being the default scope will be available at the classpath during the compilation as well as during runtime.
  • Runtime – The presence of this scope ensures that the dependency will be available at the runtime and not during the compile-time. For example, one may use the Servlet API during the compilation phase, however the Servlet implementation (from Sun, Oracle, IBM etc) will be required during the runtime only.
  • Test – This scope ensures that the dependency is available during the compilation of the test resources as well as during the execution of the test resources.
  • Provided – This dependency is mostly used while developing J2EE applications where the dependency is available as part of the J2EE container (or any Container) and it is not necessary for the project to package the dependency as part of the artifact. For example, while developing Enterprise bean applications, as part of the project, we will be using EJB jar for compilation; however, we never package this jar as part of the project because the EJB container will provide this jar.

Build Life-cycle

A life-cycle defines the execution for achieving a set of goals. Each life-cycle defines a sequence of phases for achieving the goals. There are three standard life-cycles defined in Maven and this section provides a brief summary about them.

  • Clean
  • Build (the default life-cycle)
  • Site

Clean life-cycle

The clean life-cycle accomplishes the goal of deleting the output directory, if present. It in-turn accomplishes this goal by running the phases, pre-clean, clean and post-clean. It is possible to configure custom actions/operations in the pre-clean and the post-clean phases.

Build life-cycle

This is the default life-cycle that maven executes uses during the build process. It has a sequence of phases namely validate, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install and deploy. As you can see the list is huge, in general in this cycle, the necessary resources and sources are generated, processed, compiled and various test steps are executed before package and deployment.

Site life-cycle

This life-cycle is used to generate the documentation and reports pertaining to the project. It is also possible to customize the various aspects during site generation like the content, image, layout etc. This life-cycle contains pre-site, site, post-site and site-deploy phases.

Using Maven

In this section, we will see how to use maven for creating, compiling, building, packaging and installing a project. Download the latest version of Maven from here. Installation of maven will be as simple as un-zipping the downloaded file into a desired directory. In this example, we will be seeing the usage of various maven plugins and goals that contributes to the entire project life-cycle. Before proceeding with the sample, create an environment variable MAVEN_HOME that points to the Maven installation directory and ensure that the PATH variable contains %MAVEN_HOME/bin.

Creating a project

Execute the following command which is used to create the initial project structure.

mvn archetype:create -DgroupId=net.javabeat.emailservice -DartifactId=emailservice

Note that in archetype:create, archetype represents the plugin prefix and create represents the goal. This goal creates a project with the name emailservice. As soon as this command is executed, one can see a directory called emailservice which has the src folder for holding the main java files, along with java files representing test-cases. The POM file with the name pom.file will also be created with the structure as follows,

pom.xml

<project>
    <modelVersion>4.0.0</modelVersion>

    <groupId>net.javabeat.emailservice</groupId>
    <artifactId>emailservice</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>emailservice</name>
    <url>http://maven.apache.org</url>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

Importing the project in Eclipse

This step is optional for those who are not working with Eclipse being the IDE for developing the project. Change the current directory to emailservice and issue the following command. This goal eclipse creates the project metadata files that are complaint with Eclipse. As soon as the following goal is executed there will be two files generated in the current directory which are .project and .classpath. This is for Eclipse to understand so that the newly created project can be imported in Eclipse IDE to work with.

mvn eclipse:eclipse

Now, for importing the project into Eclipse, Open Eclipse, go to File -> Import -> Existing Projects into Workspace and navigate to the directory containing the .project and .classpath files and Click OK. Now this project gets imported into Eclipse workspace.

Adding files to project

To make the project meaningful, we will add the following files into the main source folder.

EMail.java

package net.javabeat.emailservice;

public class EMail {

    private String fromAddress;
    private String toAddress;
    private String subject;
    private String message;
    private String status;

    public String getFromAddress() {
        return fromAddress;
    }

    public void setFromAddress(String fromAddress) {
        this.fromAddress = fromAddress;
    }

    public String getToAddress() {
        return toAddress;
    }

    public void setToAddress(String toAddress) {
        this.toAddress = toAddress;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}

The above class represents the Email model object that will be used by EmailService class for sending email. Given below is the code listing for EMailService class.

EMailService.java

package net.javabeat.emailservice;

public class EMailService {

    public EMail sendEmail(String fromAddress, String toAddress, String subject, String message){

        EMail emailObject = new EMail();
        emailObject.setFromAddress(fromAddress);
        emailObject.setToAddress(toAddress);
        emailObject.setSubject(subject);
        emailObject.setMessage(message);
        emailObject.setStatus("SUCCESS");

        System.out.println("Sending email from " + fromAddress + " to " + toAddress + "with subject "
		    + subject + " having the message contents " + message);
        return emailObject;
    }
}

Note that there is no real functionality in the above, all it does is to accept the various user inputs and constructs an email object before returning it.

Compiling the project

The next step is to compile to project, by default the compilation process will create a folder called target and it will place the java class files along with any resource files, if present.

mvn compile

Note that the location of the target folder as well as the location of the target folder is also configurable.

Packaging the project

This step will package the project with the packaging type mentioned in POM, which is JAR. Before packaging it will also execute the test cases (if specified) in the test folder and the packaged jar will be placed in the target folder.

mvn package

Installing the project

The final step is to install the jar in a local repository so that other projects, if they are dependant on this project can make references to this project. The following command will install (i.e copy the jar) to the local repository.

mvn install

Note that, by default, the local repository path will be %USER_HOME%/.m2/repository.

Writing Custom Maven plugins

In this section we will see how to write custom maven plugins. It is possible that the custom maven can be made to run outside the standard life-cycle phase or it can be made to be executed as part of the standard life-cycle phase. We will see how to accomplish both of these items in the following sections.

Creating standalone plugins

In this section, we will write a custom plugin and will see how to write it in stand-alone mode – i.e running the plugin directly without establishing any dependencies on any of the standard life-cycles available in Maven.

Creating the project

The first step is to create the project containing the custom maven plugin. This project is quite different from the regular java project because it is not a regular java artifact like JAR, WAR etc. Instead it is going to be a maven plugin. Issue the following command to create the maven plugin project.

mvn archetype:create -DgroupId=net.javabeat.maven -DartifactId=environment-info -DarchetypeArtifactId=maven-archetype-mojo

The above command creates a project with the name environment-info with the standard directory layout containing the source and test resources. If you look into the POM file, the package type will be maven-plugin to indicate that this is a maven plugin project.

Importing the project into Eclipse

This step is optional and this is for someone who wishes to work with the project in Eclipse IDE. Issuing the following with the current directory being environment-info creates the Eclipse .project and .classpath files. Now, open Eclipse and go to File -> Import -> Existing Project into Workspace and specify the directory to <<path to environment-info>> directory.

mvn eclipse:eclipse

Creating the plugin

In Maven, the plugin that achieves a specific task is called by the name MOJO (Maven Old Java Object similar to POJO). A MOJO defines the goal which is nothing but the custom action which is executed upon user’s request. In this example, we will see a custom MOJO that will define the goal of storing all the environmental properties in a text file upon execution. Have a look at the following code,

EnvironmentInfoTask.java

package net.javabeat.maven;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.Iterator;
import java.util.Map;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;

/**
 * @goal env-info
 */
public class EnvironmentInfoTask extends AbstractMojo{

    /**
     * @parameter
     *     default-value="log.txt"
     *     expression="${environment.filename}"
     */
    private String fileName;

    /**
     * @parameter
     *     default-value="C:\\temp"
     *     expression="${environment.base_dir}"
     */
    private File baseDirectory;

    /**
     * @parameter
     *     default-value="true"
     *     expression="${environment.loggingRequired}"
    */
    private boolean loggingRequired;

    public void execute() throws MojoExecutionException, MojoFailureException {

        StringBuilder fileContents = new StringBuilder();

        if (baseDirectory.exists()){
            baseDirectory.mkdirs();
            log("Created base directory '" + baseDirectory.getAbsolutePath() + "'");
        }

        Map&lt;String, String&gt; environment = System.getenv();
        Iterator&lt;Map.Entry&lt;String, String&gt;&gt; entries = environment.entrySet().iterator();

        fileContents.append("Environment Information:" + newLine());
        fileContents.append("-----------------------------------------------------------" + newLine());

        while (entries.hasNext()){

            Map.Entry&lt;String, String&gt; entry = entries.next();
            fileContents.append(entry.getKey() + "-------------&gt;" + entry.getValue() + newLine());
        }

        fileContents.append("-----------------------------------------------------------" + newLine());
        writeToFile(fileContents);
        log("File contents written");
    }

    private void log(String message){

        if (loggingRequired){
            System.out.println(message);
        }
    }

    private static String newLine(){
        return System.getProperty("line.separator");
    }

    private void writeToFile(StringBuilder fileContents){

        FileWriter fWriter = null;
        BufferedWriter bWriter = null;

        try{
            fWriter = new FileWriter(baseDirectory + File.separator + fileName);
            bWriter = new BufferedWriter(fWriter);
            bWriter.write(fileContents.toString());
        }catch (Exception e){
            log("Error in writing the contents to file -&gt;" + e.getMessage());
        }finally{
            try{
                if (bWriter != null){
                    bWriter.close();
                }
                if (fWriter != null){
                    fWriter.close();
                }
            }catch (Exception e){
                log("Error in closing the resources -&gt;" + e.getMessage());
            }
        }
    }
}

The first thing to notice is the class EnvironmentInfoTask which is going to persist the environmental information into a file extends AbstractMojo class which in turn extends the Mojo interface. Note that the goal for this MOJO is annotated using XDoclet using goal attribute with the name env-info. The method that will be called during execution of the MOJO is execute(). This method does the job of persisting the environmental properties by taking information from the various properties .

  • Base Directory – the directory under which the log file name will be created which is of type ‘java.io.File’. Note that we have used XDoclet ‘parameter’ for annotating this property. This property has the default value ‘C:\\temp’ which means the this is the default location for the log file. However, this value can be overridden at runtime by specifying the property ‘environment.base_dir’ which is an expression and its value will be resolved at runtime
  • Filename – the name of the log file into which the environmental properties will be written. Note that the default value for the log file name is ‘log.txt’. However, during runtime, the value can be overridden through the property ‘environment.filename’
  • Logging Required – whether logging information on what this MOJO is doing has to be shown. The default value being true can be overridden by specifying the value false to the property ‘environment.loggingRequired’.

Compiling the project

Issue the following command for compiling the maven plugin project.

mvn compile

Packaging the project

The following command is used to package the plugin project.

mvn package

Installing the project

For installing the project into the local Maven repository, issue the following command.

mvn install

Usually, on Windows, the local repository is %USER_HOME%/.m2/repository. On Windows XP, USER_HOME will expand to "C:\\Document and Settings\\<<UserName>>", however in Windows Vista it is "C:\\Users\\<<UserName>>".

Running the project

There are several ways to run the maven plugin. We will see them one by one. Generally running a Maven plugin takes the following form,

mvn groupId:artifactId:version:goalName

In our case, the group id is net.javabeat.maven, artifact id is environment-info, version is 1.0-SNAPSHOT and goal name is env-info. So running the following command will run the custom plugin

mvn net.javabeat.maven:environment-info:1.0-SNAPSHOT:env-info

Running the above command will create a directory C:\\temp (if not present) and create a file name with log.txt with the environment information. Because logging is enabled by default, the text ‘Created base directory C:\temp‘ and ‘File contents are written’ will be displayed in the console.

It is also possible to configure the plugin by passing alternate input values. Have a look at the following command. This command specifies an alternate base directory C:\\temp-new for the log file newLog.txt with logging turned off. Note that for passing a property to a plugin the standard way is –DpropertyName=propertyValue.

mvn net.javabeat.maven:environment-info:1.0-SNAPSHOT:env-info -Denvironment.base_dir=
C:\temp-new -Denvironment.filename=newLog.txt -Denvironment.loggingRequired=false

It is also possible to omit the version information while running the plugin in which case, Maven tries to find the recent version for the plugin by comparing the version strings. In our case, this wouldn’t happen, because we have only one version which is 1.0-SNAPSHOT. So the following command will also work.

mvn net.javabeat.maven:environment-info:env-info

Another approach to run the plugin is by omitting the group id and just specifying the artifact id along with the goal name. However, for this to happen, we have to add the group id to the default list of groups that Maven will look for. Open the file %MAVEN_HOME%/conf/settings.xml and add the following line under the element pluginsGroup.

<pluginGroup>net.javabeat.maven</pluginGroup>

This ensures that we have added the group net.javabeat.maven to the list of default groups that Maven will search for, because of which the following command will work.

mvn environment-info:env-info

Attaching plugins to existing life-cycle

So far, we have seen how to run a custom plugin in stand-alone mode. This is not of much-use and Maven provides a way for binding the goals of any custom plugin into existing build cycle. For example, let us consider that we have written a custom plugin which will simply print the current date and we want this plugin to be executed during the compilation phase and the installation phase of a project. We will see how to accomplish this goal in this section.

The following listing shows the custom plugin which will simply emit the date information.

DateInfo.java

package net.javabeat.maven;

import java.util.Date;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;

/**
 * @goal date-info
 */
public class DateInfo extends AbstractMojo {

    public void execute() throws MojoExecutionException, MojoFailureException {

        Date currentDate = new Date();
        getLog().info("Current date is " + currentDate);
    }
}

Follow the same process of creating the project, then creating the plugin, compilation, packaging and installation as we have discussed before. We will see how to attach this custom plugin during the compile and the install phase. Open POM.xml file and add the following after the project dependencies element,

<build>
    <plugins>
        <plugin>
            <groupId>net.javabeat.maven</groupId>
            <artifactId>environment-info</artifactId>
            <version>1.0-SNAPSHOT</version>

            <executions>
                <execution>
                    <id>date-info-compile</id>
                    <phase>compile</phase>
                    <goals>
                        <goal>date-info</goal>
                    </goals>
                </execution>

                <execution>
                    <id>date-info-install</id>
                    <phase>install</phase>
                    <goals>
                        <goal>date-info</goal>
                    </goals>
               </execution>

            </executions>

        </plugin>
    </plugins>
</build>

Though the above XML fragment is big, it is fairly simple. First thing is, it defines the execution element where we can define goals for executions. We have defined two execution elements one for the compile phase and the other for the install phase and we have given appropriate id for each execution element. Have a note at the goals section. We have included our custom goal date-info which will display the current date information. Note that where to search for the goal’s group id and the artifact id along with optional version is defined just above the executions element.

After this whenever we run mvn compile and mvn install for any project that contains the above definition we will see that the goal date-info getting invoked during the phases.

Conclusion

In this article, we have seen how to use Maven for creating projects and to manage dependencies between projects. We have seen Maven simplifying the task of creating, compiling, packaging, installing projects thereby reducing the pain from developers and build managers. Also discussed in the article are the extension capabilities provided by Maven for pluging-in custom plugins in the form of MOJOs.

Comments

comments

About Krishna Srinivasan

He is Founder and Chief Editor of JavaBeat. He has more than 8+ years of experience on developing Web applications. He writes about Spring, DOJO, JSF, Hibernate and many other emerging technologies in this blog.

Speak Your Mind

*