Search

November 17, 2013

Reusing JavaScript Modules in Liferay 6.2

Liferay is a JSR-286 compliant portal server that runs on a variety of different application servers. Previously I have explained how to get Liferay working with JBoss 7.2.0.

Today I would like to focus on getting an example portlet working with Liferay which uses a custom JavaScript module, similar to what we did with the GateIn portal server earlier. Since developing that example I have learned that ninjas are not always well-behaved, so we will stick to some home-grown JavaScript. We will use JavaScript to place a quote on the screen when the user clicks on a certain area of the portlet.

First things first: Liferay does a fair amount of caching for performance reasons. We will need to deactivate the caching otherwise there will be a great deal of pulling of hair and gnashing of teeth while you wonder why your newly deployed JavaScript changes have not made it to the browser. Fortunately Liferay makes this easy by allowing you to pass the flag -Dexternal-properties=portal-developer.properties to the application server on start-up. This incorporates the properties in the /WEB-INF/classes/portal-developer.properties file from the Liferay WAR into the Liferay system properties. If, for whatever reason you require more fine-grained control, you can manipulate the same properties by placing them in your portal-ext.properties file in your Liferay home directory (typically one directory above your application server home directory).

Now that we have an environment conducive to peaceful code development, we can begin. Liferay incorporates the AlloyUI JavaScript library, which is based on the YUI library from Yahoo (specifically the 3.x series sometimes referred to as YUI3, particularly when using a search engine). AlloyUI inherits the module model from YUI and that gives us a nice scaffold to organize our JavaScript.

Unlike GateIn however, Liferay is not "module-aware". That is, we cannot declare our modules and then declare which modules a particular portlet is dependent on. So we’ll need to work around that by placing our modules in a Liferay hooks. Hooks allow us to extend or override core features of Liferay without needing to change Liferay’s source. In our case we will simply use the hook functionality to include our module’s JavaScript in the portal template.

The easy way to get started with a Liferay hook is to use the Liferay IDE, a set of extensions for Eclipse. But I have never been a big fan of Eclipse and I am a big fan of doing things manually at least once for better understanding. So I’ll explain how to do the "hard" way (which really isn’t all that bad anyway.

Create a WAR project (using your favorite method) to hold the hook. Add a /WEB-INF/web.xml file with an empty <web-app> tag:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" metadata-complete="true"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
           http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
</web-app>

Next we’ll need a /WEB-INF/liferay-hook.xml file telling Liferay where to find our JSP extensions. Liferay has documentation for this file and their other XML configuration files at http://docs.liferay.com/portal/6.2/definitions/. Here is the content of our /WEB-INF/liferay-hook.xml file:

<?xml version="1.0"?>
<!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 6.2.0//EN"
    "http://www.liferay.com/dtd/liferay-hook_6_2_0.dtd">
<hook>
    <custom-jsp-dir>/WEB-INF/jsp</custom-jsp-dir>
</hook>

Now we need to create the JSP extension file which Liferay will append to the standard JSP. Create /WEB-INF/jsp/html/common/themes/top_js-ext.jspf:

<script type="text/javascript" src="/js/quotify.js"></script>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>

The first line is for a JavaScript file we will include in the WAR we created. The second line is an example of including a third-party JavaScript library which is hosted elsewhere.

Finally we will add our YUI JavaScript module source in /WEB-INF/jsp/js/quotify.js:

YUI().add('com.example.quotify', function(Y) {
  Y.namespace('com.example');
  Y.com.example.quotify = {
    quotes : [
"If at first you don't succeed... so much for sky-diving. -Henny Youngman",
"I intend to live forever.  So far, so good. -Steven Wright",
"A day without sunshine is like, you know, night. -Steve Martin",
"Get your facts first, then you can distort them as you please. -Mark Twain"],
    fontSizes: ['300%', '400%', '400%', '600%'],
    colors: ['black', 'black', 'blue', 'darkblue', 'darkgreen', 'darkred'],
    fontStyles: [ 'normal', 'italic', 'normal', 'oblique' ],
    fontVariants: [ 'normal', 'normal', 'normal', 'small-caps' ],
    fontWeights: [ 'normal', 'bold', 'bold', 'bolder', 'bolder' ],
    fonts: [ 'serif', 'sans-serif', 'cursive', 'monospace' ],

    random : function(array) {
      return array[Math.floor(Math.random()*array.length)];
    },

    add : function(x, y) {
      var quote = this.random(this.quotes);
      var style = "position:absolute; left:50px; top:" + (y-50) + "px;"
        + "line-height:1.1em; background-color:rgba(128,128,128,.35);"
        + "z-index:9999; width:75%; border-radius:.3em; padding: 20px;";
      var div = Y.Node.create('<div style="' + style + '"></div>');
      div.setStyle('fontSize', this.random(this.fontSizes));

      var bodyNode = Y.one(document.body);
      bodyNode.append(div);

      var me = this;
      var data = {
        words: quote.split(' ').reverse(),
        append: function() {
          var span = Y.Node.create("<span>"
            + Y.Escape.html(this.words.pop()) + " </span>");
          span.setStyle('color', me.random(me.colors));
          span.setStyle('fontFamily', me.random(me.fonts));
          span.setStyle('fontVariant', me.random(me.fontVariants));
          span.setStyle('fontWeight', me.random(me.fontWeights));
          div.append(span);
          // Let's try to give the quote presentation the cadence of Capt. Kirk
          var f = [Math.floor(2*Math.random()), Math.floor(2*Math.random())];
          var r = [300*Math.random(), 200*Math.random(), 1000];
          var when = Math.floor(r[0] + f[0]*(r[1] + f[1]*r[2]));
          if (this.words.length > 0) {
            Y.later(when, this, 'append');
          }
        }
      };
      data.append();
      Y.later(30*1000, this, function(){bodyNode.removeChild(div)});
    },

    quotesOnClick : function (node) {
      var quotify = this;
      node.on('click', function(e) {
        quotify.add(e.pageX, e.pageY);
        e.preventDefault(); // Stop the event's default behavior
        e.stopPropagation(); // Stop the event from bubbling up
      });
    }
  };
}, '1.0', {
    requires: ['node', 'escape', 'yui-later', 'event']
});

A quick primer in case you are not familiar with YUI module system: the module is defined using the YUI.add() function (an instance of YUI is obtained via the global YUI() function). The first argument gives the module name, here we have used com.example.quotify. The second argument is a function that will only be invoked when the module is required. The next argument is an unused version number for the module, then finally an array of modules the new module depends on. Within the module definition function, you are expected to set a property on the passed YUI instance containing your module. Since we are using a multilevel namespace, we use the YUI.namespace() function to ensure it exists. Then we can set the Y.com.example.quotify property. We will get to using the module soon.

That is everything we need for the Liferay hook. Of course, hooks in Liferay provide a great deal more functionality then what we have used here, but this post is going to be long enough as it is. Package your WAR and place it in the Liferay deploy directory.

Now we are ready to create our portlet. Start a new WAR project. Let’s begin with the JavaScript for the portlet which will give a chance to see how to use YUI modules. Create a file named js/portlet.js:

YUI().use(['com.example.quotify', 'node', 'event'], function(Y) {
    Y.on('domready', function() {
        Y.all('.quoteme').each(function(node, index, list) {
            Y.com.example.quotify.quotesOnClick(node);
        });
    });
});

In order to use a YUI module, we invoke the YUI.use() function. The first argument is an array of module names we would like to use. The second argument is a function accepting a YUI instance. The YUI system will pass a YUI instance that has had the requested modules initialized. This instance will be distinct to this YUI.use() invocation; if YUI.use() is used a second time to utilize the module that use will get a distinct instance of Y.com.example.quotify. (The word use now has no meaning to me after that paragraph. I’ll try not to u— include it again.)

Next we tell Liferay to insert a reference to our JavaScript file in the header for any page containing our portlet. This is done in the /WEB-INF/liferay-portlet.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE liferay-portlet-app
        PUBLIC "-//Liferay//DTD Portlet Application 6.2.0//EN"
        "http://www.liferay.com/dtd/liferay-portlet-app_6_2_0.dtd">
<liferay-portlet-app>
    <portlet>
        <!-- TODO: Use the name of your portlet from the portlet.xml here -->
        <portlet-name>name of your portlet from portlet.xml</portlet-name>
        <instanceable>true</instanceable>
        <header-portlet-javascript>/js/portlet.js</header-portlet-javascript>
    </portlet>
</liferay-portlet-app>

Here we reference our JavaScript file in the <header-portlet-javascript> tag. The <instanceable> tag indicates to Liferay whether the portlet may be included on the page once (false) or many times (true). You may need to adjust it depending on whatever portlet you decided to turn into Frankenstein’s monster by sewing my quotify module onto it.

Next make sure that your portlet output has some <span> or <div> tags with the quoteme class defined, e.g. <span class="quoteme">I’m just some innocent text</span>. The Y.on(domready,…) idiom executes the function body when the DOM is available; at that point onclick handlers are added to every DOM element with the quoteme class defined.

In summary, Liferay’s support for JavaScript modules, while not as integrated as GateIn’s, is quite serviceable. Liferay supports multiple hooks on a single server, so you can have a separate hook for each JavaScript module if you want and you can easily distribute your modules to third parties as well.

One advantage of GateIn’s approach over the approach above is that GateIn can dynamically include the necessary JavaScript files on the page on demand and omit the unnecessary. However, given the extensibility of the Liferay portal, it is not hard to imagine implementing hooks or extensions it to handle modules the same way. One would need to hook into the deployment event to scan and register the modules and then include the right <script> tags on render.