Creating a Liferay portlet based on AngularJS

After my post about converting an existing AngularJS app into a portlet I’ve got a lot of requests about how to integrate the portlet with the portal backend.
So I’ve created a little demonstrator and put in on GitHub: https://github.com/nonblocking/liferay-angularjs-portlet. It uses Spring Portlet MVC and thymeleaf as template engine as backend.

Communication to the backend

The AJAX communication with the backend is based on resource requests which are originally intended to serve static resources such as images and JavaScript (see: http://www.oracle.com/technetwork/java/jsr286-2-141964.html#Serving_Resources).

To pass the resource URL (which is generated and quite complicated) to the AngularJS app the demonstrator uses thymeleaf’s Script Inlining support. Within the head section of index.html:

<script type="text/javascript" th:inline="javascript">
  var angularJsPortletAjaxURL = /*[[${ajaxURL}]]*/ "/testdata/";
  var angularJsPortletStandalone = /*[[${standalone}]]*/ true;
  var angularJsPortletAuthenticatedUser = /*[[${authenticatedUser}]]*/ "anonymous";
</script>

And within the controller method responsible for the View phase:

@RenderMapping
public String view(RenderRequest request, RenderResponse response, ModelMap model) {
  User user = (User) request.getAttribute(WebKeys.USER);
  String userScreenName = user != null ? user.getScreenName() : "anonymous";

  ResourceURL baseResourceUrl = response.createResourceURL();

  model.addAttribute("ajaxURL", baseResourceUrl.toString());
  model.addAttribute("standalone", false);
  model.addAttribute("authenticatedUser", userScreenName);

  return "index";
}

To invoke a specifc controller method and fetch some data a POST request is sent to the resource URL extended by the resource id (&p_p_resource_id={myResourceId}).
The code within AngularJS looks like this:

var method = 'users';
var params = { "startIndex": startIndex, "limit": limit };

$http({
  url: angularJsPortletAjaxURL + "&p_p_resource_id=" + method,
  method: 'POST',
  params: params
})
.success(function(data) { 
/* ... */ 
});

And the AJAX call will invoke the controller method with the corresponding ResourceMapping annotation:

@RenderMapping
@ResourceMapping("users")
public void users(@RequestParam int startIndex, 
  @RequestParam int limit, ResourceResponse response) throws Exception {
    UserList users = /* fetch the users */

    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");

    //Automatic JSON serialization doesn't work yet in Protlet MVC
    //See: https://jira.spring.io/browse/SPR-7344
    JSON_MAPPER.writeValue(
      response.getPortletOutputStream(), users);
}

Javascript and CSS resources

The JavaScript and CSS resources are not directly added to the HTML, but rather declared in liferay-portlet.xml like this:

<portlet>
  <!-- ... --> 
  <header-portlet-css>/css/app.css</header-portlet-css>
  <footer-portlet-javascript>/js/vendor.js</footer-portlet-javascript>
  <footer-portlet-javascript>/js/app.js</footer-portlet-javascript>
</portlet>

Standalone Mode

A very interesting opportunity when developing a HTML5 portlet is that you can run it standalone. This greatly accelerates the development. Since thymeleaf templates are pure HTML it’s no big deal at all.

In the demonstrator I’ve added the JavaScript and CSS resources, which are normally added by the portal, to the HTML (index.html) too. But only in the case the app runs in standalone mode. If the standalone property is set within the portlet controller the script tag is not rendered:

 <script type="application/javascript" src="js/app.js" 
   th:if="${standalone}">
</script>

Secondly I’ve created a test backend which delivers test data (JSON) when the portal is not available. There actual implementation is selected based on the global angularJsPortletStandalone variable which will also be rewritten by thymeleaf:

.factory('backend', function($http) {
  var portletBackend = /* ... */;

  var testBackend = /* ... */;

  if (angularJsPortletStandalone) {
    return testBackend;
  } else {
    return portletBackend;
  }
});

The test backend also comes in handy when you use Jasmine to unit test your JavaScript code (something you should definitely consider).

I hope the demo portlet is helpful. Don’t hesitate to post remarks and comments.

Bringing the Java and Javascript world together

Many companies with Java as primary platform nowadays start to write pure HTML5/JavaScript front-ends, which is basically a good idea, since most Java Web Frameworks are quite painful.

The question is, how to integrate such an App into the existing Maven/Graddle build and how to deploy it on the application server. Since JavaScript has its own ecosystem of build and packaging systems, such as Node.js, Grunt or Bower.

One approach, I see a lot, is to integrate HTML5/JavaScript completely into the Java Project structure and let Maven trigger the JavaScript build. The idea is to just put all the JavaScript code and assets into a Maven WAR module and use the grunt-maven-plugin, or something similar, to launch the Grunt build.

Well, that works, but I’d advice against this approach, because:

  • It’s odd to couple two full featured build systems
  • Their build phases are not synchronized: The whole Grunt build, including the JavaScript tests, is executed in the Maven Test phase (default)
  • It slows things up to launch Maven and the Java VM to just start-up V8 and Grunt
  • The developers will start to use the Java Application Server instead of node.js for development, which is significantly slower and doesn’t allowed to run jshint and the JavaScript unit tests in the background
  • As your JavaScript applications grow you might want to re-use some Code and CSS in other Apps. That’s not possible with the integrated approach.
  • You might hire some JavaScript hackers which are not pretty familiar with the Java tooling landscape.

I present here an approach, which builds the Java and JavaScript stuff separately and merges it together at the end. The steps are:

  1. On the JavaScript side:
    1. Create separated HTML5/JavaScript modules with Grunt and Bower as build and packaging system
    2. Use a Grunt compress task to create Bower compatible ZIP file as the last build step
    3. Use a custom Grunt task to generate a Maven POM file, which just includes a build-helper:attach-artifact goal to turn the Bower ZIP file into a Maven artifact
    4. Add the project as dependency to another Bower module by just stating the full URL to the generated ZIP file. Which could of course be an URL of a Nexus or Artifactory repository.
  2. On the Build Server:
    1. On your build server run mvn install in the folder of the generated POM to install the Bower ZIP as Maven artifact in your local repository
    2. Install the artifact on Nexus or Artifactory
  3. On the Java side:
    1. Add the ZIP as dependency and unpack it into your WAR module in the package phase

Create the HTML5/JavaScript projects

To make things a bit more interesting we’re going to creating two Bower modules: A Theme, with just a single CSS file, and a AngularJS App.

The Theme project contains just a css/theme.css and a bower.json configuration like this:

{
  "name": "myTheme",
  "version": "1.0.0-SNAPSHOT",
  "main": "theme.css",
  "dependencies": {
    "bootstrap": "~3.1"
  }
}

In package.json we need to add grunt-contrib-compress as a development dependency:

{
  "dependencies": {
  },
  "devDependencies": {
    "grunt": "~0.4.1",
    "bower": "~1.3",
    "grunt-contrib-cssmin": "~0.7.0",
    "grunt-contrib-compress": "~0.7.0"
  },
  "scripts": {
    "postinstall": "bower install"
  },
  "engines": {
    "node": ">=0.8.0"
  }
}

In Gruntfile.js we have to add a compress task and a custom task to generate a pom.xml:

module.exports = function (grunt) {
  grunt.loadNpmTasks('grunt-contrib-cssmin');
  grunt.loadNpmTasks('grunt-contrib-compress');
  grunt.initConfig({   
   cssmin: {
    combine: {
    files: {
      'dist/theme.css': ['css/theme.css']
    }
    }
  },
  compress: {
    main: {
     options: {
       archive: 'deploy/myTheme.zip'
     },
      files: [
        { expand: true, cwd: 'dist/', src: ['**'], dest: '/'},
        { src: ['bower.json'], dest: '/'}
      ]}
    }
  });
  
  grunt.registerTask('mavenpom', function () {
   var bowerJson = grunt.file.readJSON('bower.json');
   grunt.log.writeln('Determined project version: ' + bowerJson.version);
 
   var pom = '<?xml version="1.0" encoding="UTF-8"?>\n' +
     '<project xmlns="http://maven.apache.org/POM/4.0.0"' +
     ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
     ' xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">\n' +
     '  <modelVersion>4.0.0</modelVersion>\n' +
     '  <groupId>at.nonblocking.bower</groupId>\n' +
     '  <artifactId>myTheme</artifactId>\n' +
     '  <packaging>pom</packaging>\n' +
     '  <version>' + bowerJson.version + '</version>\n' +
     '  <build>\n' +
     '    <plugins>\n' +
     '      <plugin>\n' +
     '        <groupId>org.codehaus.mojo</groupId>\n' +
     '        <artifactId>build-helper-maven-plugin</artifactId>\n' +
     '        <version>1.8</version>\n' +
     '        <executions>\n' +
     '          <execution>\n' +
     '            <id>attach-artifacts</id>\n' +
     '            <phase>package</phase>\n' +
     '            <goals>\n' +
     '              <goal>attach-artifact</goal>\n' +
     '            </goals>\n' +
     '            <configuration>\n' +
     '              <artifacts>\n' +
     '                <artifact>\n' +
     '                  <file>deploy/myTheme.zip</file>\n' +
     '                   <type>zip</type>\n' +
     '                </artifact>\n' +
     '              </artifacts>\n' +
     '            </configuration>\n' +
     '          </execution>\n' +
     '        </executions>\n' +
     '      </plugin>\n' +
     '    </plugins>\n' +
     '  </build>\n' +
     '</project> ';
 
   grunt.log.writeln('Writing deploy/pom.xml');
   grunt.file.write('deploy/pom.xml', pom);
  });
  
  grunt.registerTask('default', [
    'cssmin',
    'compress',
    'mavenpom'
  ]);
};

Please note, the POM artifactId and version is taken from bower.json!

If we now execute:

npm install
grunt
cd deploy
mvn install

a Bower module is generated and installed it as Maven artifact.

For the actual app we can use Yeoman to generate a AngularJS hello world example:

npm install -g yo
npm install -g generator-angular  
yo angular

And should get a structure like this:

angular_app_structure

Let’s say, the myTheme project is already on an artifactory repository, then you can add it as dependency like this:

{
  "name": "myApp",
  "version": "1.0.0-SNAPSHOT",
  "dependencies": {
    "angular": "1.2.16",
    "angular-resource": "1.2.16",
    "angular-cookies": "1.2.16",
    "angular-sanitize": "1.2.16",
    "angular-route": "1.2.16"
    "myTheme": "http://myserver/artifactory/release/at/nonblocking/bower/myTheme-1.0.0-SNAPSHOT/myTheme-1.0.0-SNAPSHOT.zip"
  }
}

Furthermore you have to add the same compress and mavenpom task to Gruntfile.js as for the myTheme module.

Create the Jenkins Jobs

On Jenkins we will need two jobs to build myTheme and myApp. Just create two free-style projects, configure the source code management section accordingly and add node.js to the PATH:

jenkins_nodejs

Then create two build tasks for each job:

  1. An Execute Shell build step to start the grunt default task
  2. And a Maven step to install the generated bower ZIP locally in your repository

jenkins_grunt

The last step on Jenkins is to publish the ZIP artifact in your remote repository:
jenkins_artifactory

Create the Java WAR module

The configuration of the WAR module is straight forward: Add the ZIP files as dependencies and use the dependency plugin to unzip it into the target/projectName folder:

<dependencies>
    <dependency>
        <groupId>at.nonblocking.bower</groupId>
        <artifactId>myTheme</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <type>zip</type>
    </dependency>
    <dependency>
        <groupId>at.nonblocking.bower</groupId>
        <artifactId>myApp</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <type>zip</type>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-dependency-plugin</artifactId>
            <version>2.2</version>
            <executions>
                <execution>
                    <id>unpack-bower-bundles</id>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>unpack-dependencies</goal>
                    </goals>
                    <configuration>
                        <includeTypes>zip</includeTypes>
                        <outputDirectory>
                            ${project.build.directory}/${project.build.finalName}/
                        </outputDirectory>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

That’s it! Now your HTML5 front-end team can work completely independently from your Java back-end team. And both teams will use the existing Maven repository.

Unit Testing of Spatial Queries

One reason why native SQL should be avoided is that it is not unit testable, so you never will get fast feedback if you break some query or the application model does no longer match the DB model. Another reason is, that your customers might already have a database vendor of their choice and you don’t want to force them to buy expensive licences from another one.

Even for spatial queries it is possible to use an abstraction layer and support multiple database vendors. Thanks to Hiberante Spatial or Eclipse Link extensions.

And if you use Hibernate Spatial in conjunction with GeoDB, a spatial extension of H2, which can run In-Memory, it is possible to execute spatial queries within unit tests. Which is a beautiful thing.

Here as an example an Entity with some spatial information (a location point) and a Repository based on JPA:

package at.nonblocking.geodb;

import com.vividsolutions.jts.geom.Point;

@Entity
public class Airport {

  @Id
  @GeneratedValue
  private long id;

  private String icaoCode;
  private String iataCode;
  private String name;

  @Type(type = "org.hibernate.spatial.GeometryType")
  private Point location;

  public Airport() {}

  public Airport(String icaoCode, String iataCode, String name, Point location) {
      this.icaoCode = icaoCode;
      this.iataCode = iataCode;
      this.name = name;
      this.location = location;
  }

  //Getter and setters omitted
}
package at.nonblocking.geodb;

import com.vividsolutions.jts.geom.Geometry;

@Repository
public class AirportRepositoryJPAImpl implements AirportRepository {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Airport getById(long id) {
        return this.entityManager.find(Airport.class, id);
    }

    @Override
    public List<Airport> findWithin(Geometry shape) {
        return this.entityManager.createQuery(
            "from Airport where within(location, :shape) = true", 
               Airport.class)
            .setParameter("shape", shape)
            .getResultList();
    }
}

Hibernate Spatial comes with several geometry types you have to annotate it with @Type(type = “org.hibernate.spatial.GeometryType”). Furthermore it adds some spatial functions to JPQL, such as within(), intersect() and so on.

To be able to write a unit tests against the Repository above, first add the following dependencies to your POM file:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.1.4.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-spatial</artifactId>
    <version>4.0</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>4.0.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>4.0.2.RELEASE</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.opengeo</groupId>
    <artifactId>geodb</artifactId>
    <version>0.8</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
</dependency>

You might have to add the following repository for GeoDB:

  <repository>
    <id>opengeo.org</id>
     <url>http://repo.opengeo.org/</url>
  </repository>

Next create the following Spring context to bootstrap GeoDB and Hibernate:

<bean id="airportRepository" class="at.nonblocking.geodb.AirportRepositoryJPAImpl"/>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="persistenceProviderClass" value="org.hibernate.ejb.HibernatePersistence"/>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
    </property>
    <property name="jpaPropertyMap">
        <map>
            <entry key="hibernate.dialect" 
              value="org.hibernate.spatial.dialect.h2geodb.GeoDBDialect" />
            <entry key="hibernate.hbm2ddl.auto" value="update" />
            <entry key="hibernate.show_sql" value="true" />
            <entry key="hibernate.format_sql" value="true" />
        </map>
    </property>
    <property name="packagesToScan">
        <list>
            <value>at.nonblocking.geodb</value>
        </list>
    </property>
</bean>

<bean id="dataSource" class="at.nonblocking.geodb.GeoDBInMemoryDataSource" />

<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

The only difference to a normal Hibernate 4 setup is the dialect org.hibernate.spatial.dialect.h2geodb.GeoDBDialect.

For GeoDB it was necessary to create a custom data source, because all connections need to be initialized through GeoDB.init(). GeoDBInMemoryDataSource looks like this:

import geodb.GeoDB;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;

public class GeoDBInMemoryDataSource extends SingleConnectionDataSource {

    public GeoDBInMemoryDataSource() {
        setDriverClassName("org.h2.Driver");
        setUrl("jdbc:h2:mem:test");
        setSuppressClose(true);
    }

    @Override
    public Connection getConnection() throws SQLException {
        Connection conn = super.getConnection();
        GeoDB.InitGeoDB(conn);
        return conn;
    }
}

And now you can use Spring Test to write a JUnit test:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-context-geodb2.xml")
@TransactionConfiguration(defaultRollback = true)
public class InMemoryGeoDbSpatial2Test {

    @Resource
    private AirportRepository airportRepository;

    @PersistenceContext
    private EntityManager entityManager;

    @Before
    public void insertTestData() {
        GeometryFactory geometryFactory = new GeometryFactory();

        Airport airportVienna = new Airport("VIE", "LOWW", "Wien Schwechat",
          geometryFactory.createPoint(new Coordinate(16.569613472222223, 48.11034788888889)));
        Airport airportPrague = new Airport("PRG", "LKPR", "Prague Ruzyne", 
          geometryFactory.createPoint(new Coordinate(14.26, 50.100833333333334)));

        this.entityManager.persist(airportVienna);
        this.entityManager.persist(airportPrague);
    }

    @Test
    @Transactional
    public void testFindAirportsAroundVienna() {
        GeometricShapeFactory shapeFactory = new GeometricShapeFactory();
        shapeFactory.setNumPoints(32);
        shapeFactory.setCentre(new Coordinate(16.366667, 48.2));
        shapeFactory.setSize(0.5);

        Geometry aroundViennaShape = shapeFactory.createCircle();

        List<Airport> airports = this.airportRepository.findWithin(aroundViennaShape);

        assertNotNull(airports);
        assertTrue(airports.size() == 1);
        assertEquals("Wien Schwechat", airports.get(0).getName());
    }
}

And here the Hibernate SQL output:

    select
        airport0_.id as id0_,
        airport0_.iataCode as iataCode0_,
        airport0_.icaoCode as icaoCode0_,
        airport0_.location as location0_,
        airport0_.name as name0_ 
    from
        Airport airport0_ 
    where
        ST_Within(airport0_.location, ?)=1

You can see, Hibernate Spatial translates the JPQL within() function to ST_Within. On Oracle it would be translated to MDSYS.OGC_WITHIN.

Have fun with unit testing spatial queries.