Extending the Wisdom Build Process

Wisdom lets you extend the build process and react to file updates to improve the development experience. The Wisdom build system is based on Apache Maven, and so, to extend it you just implement a regular Maven Plugin, aka Mojo.

This section does not explain how to develop a regular Maven plugin. Refer to The Maven Mojo Developer Documentation for further details. However, it explains how your Mojo can participate in the Wisdom Watch Mode.

Rationale

We are often asked why we want a Watch Mode based on Maven, and not something separated, as most of frameworks do. This is because duplicating the build process creates a strange experience. The result looks the same, but it may not really be the same. Debugging such kind of differences is almost impossible. So in Wisdom, Maven is the reference and the Watch Mode is built on top of it. During the Watch Mode, Wisdom invokes the Maven Mojos directly.

This also means that using your Wisdom Build extension is just like using any other Maven plugin: * add the plugin in the pom.xml file * configure it * you’re done!

From Mojo To Watcher

The Wisdom Watch Mode is based on the concept of Watcher. A Watcher is an object who is notified when a file, accepted by the watcher, is created, updated or deleted.

So, first, in your Mojo’s project, add the dependency on the Wisdom-Maven-Plugin:

<dependency>
    <groupId>org.wisdom-framework</groupId>
    <artifactId>wisdom-maven-plugin</artifactId>
    <version>0.6.1</version>
</dependency>

To make your Mojo participate in the Wisdom Watch Mode, it needs to become a Watcher and registers itself in the watch pipeline. In order to do that, the easiest way is to make your Mojo extend AbstractWisdomWatcherMojo instead of AbstractMojo.

Tip
The Pipeline is the ordered sequence of Watchers invoked in the Watch Mode. This list contains Mojo instances (that are also Watcher). This list is collected during the regular execution of your build. So the order is the same as the Maven plugin’s order.

By extending AbstractWisdomWatcherMojo, it automatically registers your Mojo to the Pipeline.

As a Watcher your mojo has to implement the following methods:

    /**
     * Checks whether the given file is managed by the current watcher. Notice that implementation must not check
     * for the existence of the file as this method is also called for deleted files.
     *
     * @param file is the file.
     * @return {@literal true} if the watcher is interested in being notified on an event
     attached to the given file,
     * {@literal false} otherwise.
     */
    public boolean accept(File file);

    /**
     * Notifies the watcher that a new file is created.
     *
     * @param file is the file.
     * @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should
     * return {@literal true} to let other watchers be notified.
     * @throws WatchingException if the watcher failed to process the given file.
     */
    public boolean fileCreated(File file) throws WatchingException;

    /**
     * Notifies the watcher that a file has been modified.
     *
     * @param file is the file.
     * @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should
     * returns {@literal true} to let other watchers to be notified.
     * @throws WatchingException if the watcher failed to process the given file.
     */
    public boolean fileUpdated(File file) throws WatchingException;

    /**
     * Notifies the watcher that a file was deleted.
     *
     * @param file the file
     * @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should
     * return {@literal true} to let other watchers be notified.
     * @throws WatchingException if the watcher failed to process the given file.
     */
    public boolean fileDeleted(File file) throws WatchingException;

The accept method let you select the file the watcher handles. For instance, if you want to handle .css files, your accept method will be similar to:

@Override
public boolean accept(File file) {
    return
        (WatcherUtils.isInDirectory(file, WatcherUtils.getInternalAssetsSource(basedir))
            || (WatcherUtils.isInDirectory(file, WatcherUtils.getExternalAssetsSource(basedir)))
        )
        && WatcherUtils.hasExtension(file, "css");
    }

It first checks the location of the file. In this example, it checks that the file is either in the internal assets, i .e. src/main/resources/assets or in the global assets (src/main/assets). Then, it checks for the file’s extension. Notice that you must not check for the existence, as the accept method is also called for deleted file.

Once the accept method is implemented, you need to implement the:

  • fileCreated(File file) : called when an accepted file is created

  • fileUpdated(File file) : called when an accepted file is updated

  • fileDeleted(File file) : called when an accepted file is deleted

In general, the fileCreated and fileUpdated methods call the Mojo’s main method (execute), while the fileDeleted method cleans up generated files when the input file is deleted.

These methods return a boolean controlling the pipeline execution. If one of the method returns false, the pipeline is interrupted, and none of the following watchers are called. In most cases, you need to return true.

Handling errors : the Watching Exceptions

As you may have noticed, the Watcher's methods can throw WatchingException. Watching Exceptions let you indicate an issue in the processing. A default error page is generated from this exception with the message, of the guilty file, and if specified, the line and position of the error.

Initialization and Injection

As your Watcher is a Mojo and the pipeline is reusing the Mojo’s instances, all of the injected parameters from your Mojo are still available. Notice that the execute method is called before the Watcher’s methods, meaning that you can do the required initialization there if required.

Important
The pipeline is executed by one thread only (no parallel execution).

Disabling the Watch Mode

Maybe your Mojo is skipped. In that case you need to explicitly unregister it from the pipeline by calling the removeFromWatching method in your execute method.

Generating a Watcher using the Maven Archetype

We provide a Maven Archetype generating the project to develop your Watcher. Executes the following command to generate it:

mvn org.apache.maven.plugins:maven-archetype-plugin:generate \
 -DarchetypeArtifactId=wisdom-simple-watcher-archetype \
 -DarchetypeGroupId=org.wisdom-framework \
 -DarchetypeVersion=0.6.1 \
 -DgroupId=YOUR_GROUPID \
 -DartifactId=YOUR_ARTIFACTID \
 -Dversion=1.0-SNAPSHOT

Using Node and NPMs

Most of the Web Tools developed today are available as NPM (i.e. Node’s module). Fortunately, Wisdom provides a set of utility classes to install and execute NPM.

Important
Wisdom has it’s own node version installed in /.wisdom/node/Node_Version. So, the NPMs you are installing and executing do not conflict with the user’s ones. These NPM are installed in / .wisdom/node/Node_Version/lib/node_modules+.
Important
The Wisdom’s NPM support only work for executable NPM’s.

To use a NPM tool, declare a NPM object as follows:

NPM myNPM = npm(this, npm_name, version);

In most cases, declare your NPM object as a field, as both the execute method and watcher’s method will invoke it. If the specified NPM is not install yet, it will install it (from NPM).

It is a good practice to declare the NPM’s version as a parameter, so the user can specify the version:

@Parameter(defaultValue = "0.3.4")
String version;

To execute the NPM, use:

myNPM.execute("exec", input.getAbsolutePath(), destination.getAbsolutePath() ...);

The "exec" argument is the name of the command to launch. Available commands are available from the package.json files of the NPM, in the bin part:

 "bin": {
     "myth": "bin/myth"
 },

The others arguments are the command’s arguments. In the previous example the input and output paths.

So, for instance, here are the lines invoking the myth processing:

try {
    int exit = myth.execute("myth", input.getAbsolutePath(), destination.getAbsolutePath());
    getLog().debug("Myth execution exiting with status: " + exit);
} catch (MojoExecutionException e) {
    throw new WatchingException("An error occurred during Myth processing of " + input.getAbsolutePath(), e);
}

The CoffeeScript invocation is made by:

try {
    int exit = coffee.execute(COFFEE_SCRIPT_COMMAND, "--compile", "--map", "--output", out.getAbsolutePath(),
            input.getAbsolutePath());
    getLog().debug("CoffeeScript compilation exits with " + exit + " status");
} catch (MojoExecutionException e) {
    if (!Strings.isNullOrEmpty(coffee.getLastErrorStream())) {
        throw buildCompilationError(coffee.getLastErrorStream(), input);
    } else {
        throw new WatchingException(ERROR_TITLE, "Error while compiling " + input
                .getAbsolutePath(), input, e);
    }
}

In this last example, we retrieve the error stream (if any) and parse the error message to build a WatchingException containing the compilation error, the guilty line and position.