Sunday 26 October 2014

Ontrack 2.0.0-rc has been released

Ontrack 2.0.0-RC was just released.

Main new features:
  • branch templating
  • possibility to disable projects and branches
  • build filters can be shared for a branch
  • easier update of a branch and all its components
The complete list of features is available at https://github.com/nemerosa/ontrack/releases/tag/2.0.0-rc-101

Please note that this is a release candidate, before 2.0.0 is out.

Ontrack project is available at http://nemerosa.github.io/ontrack/

Best regards,
Damien.

Thursday 2 October 2014

ontrack 2.0.0-beta-8 released

Ontrack 2.0.0-beta-8 was just released.

Main new features:

  • integration with a LDAP for authentication
  • display of all the events in their context so that you can keep track of what has happened
  • cloning features for projects and branches
The complete list of features is available at https://github.com/nemerosa/ontrack/releases/tag/2.0.0-beta-8-74

Please note that this is the last BETA release. I plan on doing one last release candidate before going in full maintenance mode.

Ontrack project is available at http://nemerosa.github.io/ontrack/

Best regards,
Damien.

Tuesday 13 May 2014

Spring Boot + Grunt + Bower

After I wrote some comments about the integration of Grunt + Bower with Maven, it happens that I had to apply this in the context of Spring Boot.

Out of the box, Spring Boot allows you serve some static content by placing it in the resources under /static (or /public, or other locations, but let's stick to /static in this blog entry). It is all very nice if you need only to serve a few pages and files, but if you need to code a complex Javascript application and wish to use proper tools like Grunt or Bower, you may have an issue.

It seems obvious that you cannot setup your Grunt project directly under the /src/main/resources/static directory. That would mean an awkward SCM setup and an even more awkward URL pattern.

What I chose to do was to create a separate Maven module to hold my Grunt project, and to follow the guidelines I had established previously, with a few differences.

The general idea is that in development mode, the Spring Boot application would serve the static content directly from the target/dev directory of the Grunt module, and that in release mode, the target/prod directory of the Grunt module would be copied into the /src/main/resources/static directory of the Spring Boot module, and packaged together with the resulting JAR file.

Development mode

In development mode, I needed to find a way to tell Spring Boot to serve the static content from target/dev directory of the Grunt module. It is easily done by defining a ResourceHandler for the development profile only:

@Configuration
@Profile("dev")
public class DevWebConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private DevSettings devSettings;

    /**
     * At development time, we want the static resources served directly
     * from the <code>myproject-web</code> project, under the <code>target/dev</code>
     * directory.
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        String staticDir = devSettings.getStaticDir();
        String prefix = "file:";
        registry.addResourceHandler("/app/**").addResourceLocations(prefix + staticDir + "/app/");
        registry.addResourceHandler("/assets/**").addResourceLocations(prefix + staticDir + "/assets/");
        registry.addResourceHandler("/vendor/**").addResourceLocations(prefix + staticDir + "/vendor/");
        registry.addResourceHandler("index.html").addResourceLocations(prefix + staticDir + "/index.html");
    }
}

Additionally, I needed a configuration object to map the application properties:

@Component
@ConfigurationProperties(prefix = "myproject.dev")
public class DevSettings {

    private String target;
    private String staticDir;

    // Getters & setters...
    
}

In the application-dev.properties, activated by the "dev" profile, I just define the following properties:

myproject.dev.target=target/dev
myproject.dev.staticDir=${user.dir}/myproject-web/${myproject.dev.target}

When I want to run the application, I just have to run the Web module in watch mode:

cd myproject-web
grunt watch

and to launch my Boot application with "dev" profile enable by adding --spring.profiles.active=dev to the program arguments.

Release mode

When the application has to be released, I active a release Maven profile in order to enable the following code in the Spring Boot POM:

<profile>
    <id>release</id>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>release-web-resources</id>
                            <phase>compile</phase>
                            <goals>
                                <goal>copy-resources</goal>
                            </goals>
                            <configuration>
                                <outputDirectory>
                                   ${project.basedir}/src/main/resources/static
                                </outputDirectory>
                                <resources>
                                    <resource>
                                        <directory>
                                          ../myproject-web/target/prod
                                        </directory>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</profile>

I have added the myproject-web Grunt module into my dependencies in order to make sure it is ready before the copy:

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>myproject-web</artifactId>
    <version>${project.version}</version>
    <scope>runtime</scope>
</dependency>

And that's it! Again, in order to see the details of how the Grunt project itself is integrated with the Maven lifecyle, have a look at Grunt + Bower with Maven.

Tuesday 15 April 2014

Grunt + Bower + Maven

iTeach is a web application that helps independent teachers to organise their lessons and their agenda. It is composed of two main elements:
  • 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>

Here we are.

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.

Monday 14 April 2014

Dynamic locale in AngularJS

As soon as one starts to deal with a multilingual application in AngularJS, he is faced with the fact that the $locale service is a read-only service.

As a end user of the application, I want to select the language I'm working with, and see the changes to the screen immediately applied.

In particular, I was also using the excellent angular-translate module for having my labels translated. I could change my locale for the translation module using $translate.use(locale), but the AngularJS locale itself was not changed - this made localised dates & times using the former selected locale. Very not good.

Here is the solution I have found for switching the language.

In the main module file, I add, before the module declaration, a detection of the current locale. In my case, I chose to store it locally, but it could be any other mechanism:

// Local storage for the locale
if (console) console.log('[language] Checking language on load');
var language = localStorage['iteachLanguage'];
if (!language) {
    if (console) console.log('[language] No language specified, using "en"');
    language = 'en';
}
if (console) console.log('[language] Using ' + language);

Then, in the module loading itself, I load the corresponding ngLocale module (which are available though the angular-locale_**.js file distributed with Angular - pity they are not available yet through Bower):

angular.module('myapp', [
            'ui.bootstrap',
            'ui.calendar',
            'ngRoute',
            'ngSanitize',
            'ngLocale_' + language,
            'pascalprecht.translate',
            ...

Then, when the user selects a locale, I just have to store it locally and reload the page:

$scope.changeLanguage = function (lang) {
   localStorage['iteachLanguage'] = lang;
   location.reload();
};

Well, yes, I have to reload the page... But in such a case, I think this is acceptable.