- an API service
- a GUI layer that calls this API
Although the API service is a standard layered Spring MVC application, built and assembled using Maven, the GUI layer is an AngularJS one-page application, that relies on Grunt. It is however obvious that those two modules are two parts of the same application, and I needed a way to integrate seamlessly the Grunt module into a larger Maven structure.
The application is structured this way:
iteach-parent
    |-- pom.xml              // The parent POM
    |-- iteach-ui            // The WAR that serves the API
    |      |-- pom.xml
    |-- iteach-gui-static    // The GUI layer
    |      |-- pom.xml       // The GUI module is still considered a Maven module
    |      |-- Gruntfile.js  // The Grunt build file, plus other files
    |-- ... other modules
At the top level, the iteach-gui-static module is still considered a normal Maven module and is registered as such in the parent POM:
<modules>
        ...
        <module>iteach-ui</module>
        ...
        <module>iteach-gui-static</module>
</modules>
The POM file for the iteach-gui-static module uses extensively the exec-maven-plugin in order to call npm, bower and grunt in the correct phases:
<build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>prepare-env-npm</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>npm</executable>
                            <arguments>
                                <argument>install</argument>
                            </arguments>
                        </configuration>
                    </execution>
                    <execution>
                        <id>prepare-env-bower</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>bower</executable>
                            <arguments>
                                <argument>install</argument>
                                <argument>--force-latest</argument>
                            </arguments>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <profiles>
        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.codehaus.mojo</groupId>
                        <artifactId>exec-maven-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>prepare-dev</id>
                                <phase>prepare-package</phase>
                                <goals>
                                    <goal>exec</goal>
                                </goals>
                                <configuration>
                                    <executable>grunt</executable>
                                    <arguments>
                                        <argument>clean</argument>
                                        <argument>dev</argument>
                                    </arguments>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
        <profile>
            <id>release</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-antrun-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>prepare-release-version</id>
                                <phase>compile</phase>
                                <goals>
                                    <goal>run</goal>
                                </goals>
                                <configuration>
                                    <target name="replaceVersion">
                                        <echo>Replacing versions in *.json files</echo>
                                        <replaceregexp
                                                match=""version": "(.*)""
                                                replace=""version": "${project.version}""
                                                byline="true"
                                                encoding="UTF-8"
                                                >
                                            <fileset dir="${basedir}" includes="*.json" />
                                        </replaceregexp>
                                    </target>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.codehaus.mojo</groupId>
                        <artifactId>exec-maven-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>prepare-release</id>
                                <phase>compile</phase>
                                <goals>
                                    <goal>exec</goal>
                                </goals>
                                <configuration>
                                    <executable>grunt</executable>
                                    <arguments>
                                        <argument>clean</argument>
                                        <argument>prod</argument>
                                    </arguments>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
You can see the complete source code on GitHub.
In the validate phase, I run the following commands:
- npm install
- bower install --force-latest
In dev profile, during the prepare-package phase, I run the grunt clean dev command, that makes all GUI files ready in the local target/dev folder (important, see later).
In release profile, during the prepare-package phase, I do two things:
- replacing the version in the bower.json and the package.json files by the version to release
- run the grunt clean prod command, which packages and compresses all files in target/prod
Now, back in the iteach-ui module (the part that defines the actual WAR to be deployed), I introduce iteach-gui-static as a runtime dependency, in order to force the WAR to be built after the GUI layer has been prepared:
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>iteach-gui-static</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
For the release profile, I just tell Maven to copy the target/prod folder of the iteach-gui-static module just before the packaging:
<profile>
<id>release</id>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-gui-resources</id>
<phase>compile</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>src/main/webapp</outputDirectory>
<resources>
<resource>
<directory>../iteach-gui-static/target/prod</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
That's it for the release: the packaged WAR will embed the compressed static resources.
For the development mode, I'm using the tomcat7-maven-plugin and I tell Tomcat to use the target/dev folder of the iteach-gui-static module as the root of static documents:
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<path>/</path>
<contextFile>src/test/resources/tomcat/context-dev.xml</contextFile>
<warSourceDirectory>../iteach-gui-static/target/dev</warSourceDirectory>
...
</configuration>
So, to summarise my set-up:
- a normal AngularJS + Bower + Grunt application declared as a Maven module, with the correct Grunt calls mapped to Maven phases
- a dependency from the WAR to the GUI layer, with a copy of the static resources
When I develop, in a shell or in my Intellij terminal view, I just type grunt watch in the iteach-gui-static folder and launch my Web app using tomcat7:run from within my IDE. The grunt watch command launches a Livereload server that allows my browser to refresh automatically whenever I change one of my static resources. Neat.
At the end, when I release, I just have to rely on mvn package. The necessary NPM + Bower + Grunt calls will be done.
For the complete source code, go to GitHub.
