Search

November 3, 2013

Packaging GateIn JavaScript Modules in Separate WARs

I have been messing around with the GateIn Portal Server in order to evaluate it for an upcoming project. One nice aspect of the portal is the way JavaScript is handled. JavaScript in GateIn is split into modules and managed via the RequireJS library. This allows the portlet developer to keep their JavaScript isolated and only include the dependencies they require. It also allows for re-use of modules defined in one portlet in other portlets. It doesn’t take a lot of imagination to picture the disaster JavaScript could become on a portal which doesn’t provide isolation and re-use, especially if multiple organizations are providing portals.

Starting with the solid foundation provided by GateIn, we just need a couple of tweaks to make things even better. First, one potential gotcha is that the modules all live in the same namespace across the portal. So we will borrow the solution from Java and prefix our module names with the inverse of the our domain name. For the purposes of this post, I will use the prefix com.example.

The second thing is that modules are defined in portlets. This could be an issue if you create a re-usable module in portlet X, it gets used in portlet Y and then down the road portlet X is no longer needed and is removed. The solution is to package our re-usable modules in their our war files separate from the portlets.

Which leads me to the point of this post, demonstrating how to package a JavaScript module for GateIn in its own war archive. Originally inspired by the Daily WTF, I was going to create modules for adding unicorns to the portal. But after going to http://www.cornify.com to borrow the JavaScript, I discovered the much cooler http://ninjafy.com. So we will be adding ninjas to the portal instead.

Since I originally posted this entry, it has come to my attention that Google is warning users that ninjafy.com may be serving malware. So the safe thing to do would be to get your own (known safe) ninja pictures and package them with your module. Then adjust the URL parameters in the script below.

Just to demonstrate dependencies and module interaction, we will create two modules, com.example.ninjafy and com.example.module. Create a js directory under the web root of the war and add the file ninjafy.js:

// Adapted from http://ninjafy.com/js/ninjafy.js
define(function() {
    return {
        add : function() {
            var ninjafy_url = '/path/to/images/';
            var file = 'ninja_';
            var count = 7;
            file += Math.ceil(Math.random()*count) + '.gif';
            var div = document.createElement('div');
            div.style.position = 'fixed';

            var height = Math.random()*.9;
            if (typeof(window.innerHeight) == 'number') {
                height = height*window.innerHeight+'px';
            } else if(document.documentElement
                      && document.documentElement.clientHeight) {
                height = height+document.documentElement.clientHeight+'px';
            } else {
                height = height*100+'%';
            }

            div.style.top = height;
            div.style.left = Math.random()*90 + '%';
            var img = document.createElement('img');
            img.setAttribute('src',ninjafy_url+file);
            var body = document.getElementsByTagName('body')[0];
            body.appendChild(div);
            div.appendChild(img);
        }
    };
});

Here we have adapted the original ninjafy.js script into a RequireJS module. The RequireJS module takes the form of a function that returns the module. The result of the function is passed to other modules that depend on this one. We defined one function in the ninjafy module, add().

(A quick note about licensing - I couldn’t find any license information on the ninjafy site but the tone of the site certainly implies that re-using their code is encouraged.)

Next we define our second module, which will use the com.example.ninjafy modules. Create a file named example-module.js in the js directory:

define(["ninjafy"], function(ninjafy) {
    return {
        ninjasOnClick: function(element) {
            element.onclick = function() {
                ninjafy.add();
                return false;
            };
        }
    };
});

Here we can see that the com.example.ninjafy module is passed as the variable ninjafy. In this module we define a ninjasOnClick() function which adds a ninja-adding onclick handler to the given HTML element.

Next we have everybody’s favorite part of JEE development: configuring the XML files. The first two are simple. Create a web.xml that contains only a displayName element (see https://issues.jboss.org/browse/GTNPORTAL-3107). Then create an "empty" portlet.xml containing only the top-level <portlet-app> element. These xml files belong in the WEB-INF directory of the war of course.

Next we define the gatein-resources.xml file, also in the WEB-INF directory. This file defines the modules and manages their dependencies:

<?xml version="1.0" encoding="UTF-8"?>
<gatein-resources
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.gatein.org/xml/ns/gatein_resources_1_3
            http://www.gatein.org/xml/ns/gatein_resources_1_3"
        xmlns="http://www.gatein.org/xml/ns/gatein_resources_1_3">
    <module>
        <name>com.example.ninjafy</name>
        <script>
            <path>/js/ninjafy.js</path>
        </script>
    </module>
    <module>
        <name>com.example.module</name>
        <script>
            <path>/js/example-module.js</path>
        </script>
        <depends>
            <module>com.example.ninjafy</module>
            <as>ninjafy</as>
        </depends>
    </module>
</gatein-resources>

For the most part this file is self-explanatory. Note in the <depends> tag for the com.example.module module we map the com.example.ninjafy module to the name ninjafy, as expected by the example-module.js script.

Now if you build your war file and deploy it to your GateIn server, you will have a nifty new JavaScript module to play with. But what fun is a new library if we don’t actually use it? Assuming you have a working portlet to start with, I’ll show you how to use the new modules.

When creating JavaScript for portlets in GateIn, the recommended way is to define a module for the portlet. This will not be a re-usable module as before, but a module just for the portlet. Create a file named js/portlet.js (remember we are working inside the war for your portlet now, not the same war we created above to hold the JavaScript modules):

define(["example","$"], function(example, $) {
    $(document).ready(function(){
        $(".ninjame").each(function(index, element) {
            example.ninjasOnClick(element);
        });
    });
});

In this case we are expecting the com.example.module module as example and jQuery as $. GateIn supplies a jQuery module for you. (You can also use a different version of jQuery if you are picky about it, see the GateIn JavaScript documentation for details.) Our module walks the DOM and invokes our ninjaOnClick() function for each HTML element with the class ninjame.

Next you need to configure this module in the gatein-resources.xml for your portlet:

<?xml version="1.0" encoding="UTF-8"?>
<gatein-resources
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.gatein.org/xml/ns/gatein_resources_1_3
            http://www.gatein.org/xml/ns/gatein_resources_1_3"
        xmlns="http://www.gatein.org/xml/ns/gatein_resources_1_3">
    <portlet>
        <!-- TODO: Replace the following with the actual name. -->
        <name>Use the name from your portlet.xml</name>
        <module>
            <script>
                <path>/js/portlet.js</path>
            </script>
            <depends>
                <module>com.example.module</module>
                <as>example</as>
            </depends>
            <depends>
                <module>jquery</module>
                <as>$</as>
            </depends>
        </module>
    </portlet>
</gatein-resources>

Then you just need to add the CSS class ninjame to some elements in your portlet HTML. Fire up GateIn and your browser and add ninjas to your heart’s content.

A couple of final notes: I have found it is sometimes necessary to restart GateIn for changes to modules to take effect. And it is always necessary to tell your browser to reload the JavaScript after making changes (shift-reload in Firefox). Finally, GateIn by default will bundle up the shared JavaScript into one merged file which has been minimized. Adding the flag -Dexo.product.developing=true to the arguments when starting GateIn will suppress this and lead to a much better JavaScript debugging experience.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.