Wednesday, January 9, 2013

Liferay Expando Services


Liferay provides a mechanism to create tables, columns, rows and values dynamically. It provides these features through the Expando services. The mechanism is supported by using four database tables: ExpandoTable, ExpandoColumn, ExpandoRow and ExpandoValue.

Creating tables dynamically

The class com.liferay.portlet.expando.service.ExpandoTableLocalServiceUtil allows creation of dynamic tables.
To create a new table, call:
ExpandoTable table = ExpandoTableLocalServiceUtil.addTable(<companyId>, <className>, <tableName>);
Parameters:
<companyId> (Type: long) – The company id for the table. To get the company id, call
long companyId = CompanyLocalServiceUtil.getCompanies().get(0).getCompanyId();
<className> (Type: String) – The class name should be the fully qualified class name of the java class that you are trying to save to database.
<tableName> (Type: String) – The name of the dynamic table to create.
A new row is added to the ExpandoTable table.

Creating columns dynamically

The class com.liferay.portlet.expando.service.ExpandoColumnLocalServiceUtil allows creation of dynamic columns.
To create a new column, call:
ExpandoColumn column = ExpandoColumnLocalServiceUtil.addColumn(<tableId>, <columnName>, <type>);
Parameters:
<tableId> (Type: long) – The table id for which to create column.
<columnName> (Type: String) – The name of the column.
<type> (Type: int) – The data type of the column. Use the ExpandoColumnConstants to specify the data type. For example, ExpandoColumnConstants.STRING;
A new row is added to the ExpandoColumn table.

Creating rows dynamically

The class com.liferay.portlet.expando.service.ExpandoRowLocalServiceUtil allows creation of dynamic rows.
To create a new row, call:
ExpandoRow row = ExpandoRowLocalServiceUtil.addRow(<tableId>, <classPK>);
Parameters:
<tableId> (Type: long) – The table id for which to create row.
<classPK> (Type: long) – The classPK .
A new row is added to the ExpandoRow table.

Storing values in a dynamic table

The class com.liferay.portlet.expando.service.ExpandoValueLocalServiceUtil allows storing values for dynamic tables.
To store a value, call:
ExpandoValue
value = ExpandoValueLocalServiceUtil.addValue(<classNameId>,
<tableId>, <columnId>, <classPK>, <value>);
Parameters:
<classNameId> (Type: long) – The class name id.
<tableId> (Type: long) – The table id for which to store value.
<columnId> (Type: long) – The column id of the table for which to store value.
<classPK> (Type: long) – The classPK .
<value> (Type: String) – The value to be stored .
A new row is added to the ExpandoValue table. There are several variations of the addValue API to store different data types.

Removing dynamic entities

Delete Table:
ExpandoTableLocalServiceUtil.deleteTable(long <tableId>)
Delete Column:
ExpandoColumnLocalServiceUtil.deleteColumn(long <columnId>)
Delete Row:
ExpandoRowLocalServiceUtil.deleteRow(long <rowId>)
Delete Value:
ExpandoValueLocalServiceUtil.deleteValue(long<valueId>)
The above APIs for each entity is the simplest one. There are other variations depending on the parameters. The above APIs only removes the corresponding entity. It does not remove the related entities. For example, deleting table does not automatically delete the columns, rows and values.

Updating dynamic entities

Update Table:
ExpandoTableLocalServiceUtil.updateTable(long <tableId>, String <name>)
ExpandoTableLocalServiceUtil.updateExpandoTable(ExpandoTable <table>)
Update Column:
ExpandoColumnLocalServiceUtil.updateColumn(<...>)
ExpandoColumnLocalServiceUtil.updateExpandoColumn(<...>)
Update Row:
ExpandoRowLocalServiceUtil.updateExpandoRow(<...>)
Update Value:
ExpandoValueLocalServiceUtil.updateExpandoValue(<...>)

Searching for entities

Search Table:
ExpandoTableLocalServiceUtil.getTable(<...>)
ExpandoTableLocalServiceUtil.getExpandoTable(long <tableId>)
Search Column:
ExpandoColumnLocalServiceUtil.getColumn(<...>)
ExpandoColumnLocalServiceUtil.getExpandoColumn(long <columnId>)
Search Row:
ExpandoRowLocalServiceUtil.getRow(<...>)
ExpandoRowLocalServiceUtil.getExpandoRow(long <rowId>)
Search Value:
ExpandoValueLocalServiceUtil.getValue(<...>)
ExpandoValueLocalServiceUtil.getExpandoValue(long <valueId>)
ExpandoValueLocalServiceUtil.getData(<...>)

Dynamic queries

For dynamic queries, all the Local service util provide 2 APIs:
Expando[Table|Column|Row|Value]LocalServiceUtil.dynamicQuery(<...>)
Expando[Table|Column|Row|Value]LocalServiceUtil.dynamicQueryCount(DynamicQuery <dynamicQuery>)

Sunday, August 26, 2012

Integrating Coin Slider with Liferay 6.1.1 with zero development

In my previous blog, I demonstrated how to integrate the Coin Slider library in a Liferay portlet without having to develop any portlet. Coin Slider is a jQuery based image slider with many effects for image transition. That demonstration was based on Liferay 6.0.5. With Liferay 6.1.1 now available, I decided to try my own example and see whether the steps still hold true for the latest version of Liferay. And guess what, it does! That's the power of Liferay! Congratulations to the Liferay Team!

So here I demonstrate the same example with updated screenshots for the benefit of the community.

Note: This solution is based on Liferay 6.1.1

Download and extract the latest version of the Coin Slider library from http://workshop.rs/projects/coin-slider/

Liferay now has a single Library for storing both documents, images, video and any other media files. Create a folder slideshow in the Document and Media library of the community where you want to use the slider. Add the files coin-slider-min.js, coin-slider-styles.css, jquery-1.8.0.min.js to the slideshow folder. Create a new file javascript.js file in filesystem. Add the following code to this file:

$(document).ready(function() {
    $("#coin-slider").coinslider({ hoverPause: false });
});
 

Upload the javascript.js file to the slideshow library. The slideshow folder should now look like this:


Next, upload few images that you want to display in the slider in the Document and Media Library.


There are 8 images in the library above.

Now create a new web content structure. Name it coin-slideshow. Add xml schema definition to the structure as shown below:


Click Save when done.

Create a new web content template and name it coin-slideshow-template. Select coin-slideshow as the structure for the new template. Click the Launch Editor button. In the dialog that appears, paste the following code:


<link href="$css.getData()" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="$jquery.getData()"></script>
<script type="text/javascript" src="$coinjs.getData()"></script>
<script type="text/javascript" src="$slidestart.getData()"></script>

<div id="coin-slider">
#foreach ($item in $slide.getSiblings())
   #foreach ( $itemData in $item.getChildren() )
        #if ( "$itemData.getName()" == "href" )
          #set ( $href = $itemData.getData() )
        #elseif ( "$itemData.getName()" == "img_src" )
          #set ( $img_src = $itemData.getData() )
        #elseif ( "$itemData.getName()" == "description" )
          #set ( $description = $itemData.getData() )
        #end
    #end
    <a href="$href"><br />
        <img src="$img_src" />
        <span>$description</span>
    </a>
#end
</div>



Click Update button when done. Click Save again in the main dialog.

Now is the time to add the slider in our page. Add Web Content Display portlet to the page where you want the slider. Click the Add Web Content link in the portlet to create content for the portlet. In the new dialog box, give a name to the content, say Slideshow. Change the structure of the web content by clicking the Choose button under the Structure section on the right. Select the structure coin-slideshow that we created earlier. This will automatically choose the template that we associated with the structure. Instead of the HTML editor, a web form with fields are shown.

In the css field, choose the coin-slider-styles.css from the document library. Similarly, choose jquery-1.4.2.min.js for jquery field, coin-slider-min.js for coinjs field and javascript.js for coinstart field. For the first slide, name it slide1. Select a image from the Document and Media Library for the img_src field. Select target location when an image is clicked from the dropdown list for the href field. Type some text to be displayed for the image in the description field. Similarly, add more slides for each image that you want to be displayed.



Click Publish button to save the content and make it viewable.


Tuesday, June 14, 2011

OrientDB - Pure Java NoSQL Datastore

I have been following the so-called 'NoSQL movement' (as if such a movement exists!). In my opinion, it is just another way of storing and retrieving data. And this new (rather different, I am sure the method is not new) way is only suitable for certain type of applications. Not all applications require NoSQL-type storage and retrieval regardless of those who claim it to be. SQL and NoSQL complement each other. They are not rivals. 

Having said that, I was pretty much interested in the concepts. I love trying out new things. While I was planning the design and architecture of a new open source project that I intend to develop (It is not exactly new. I have already started it but now plan to move it to a new architecture - Greenscape), one of the choices I had to make was on the data storage. My application will be highly dynamic with ability of end users to add and remove columns to a database table frequently. Doing it in relational database will incur heavy performance and complexity. After much consideration, I decided that a non-relational database will be the best choice for my application. But this choice is not without its share of challenges:
  1. I loose JPA/Hibernate support which means I may have to come up with an equivalent framework.
  2. I loose portability. NoSQL is non-standard. There is no standard query language. There are no standard protocol. So, tomorrow, I cannot replace the database with a better performing one without rewriting my application.
  3. Few documentation. What are the best practices, design patterns, etc?
In spite of the challenges, I went ahead with my choice. The compelling reason was the ability to create knowledge because of the void.

Now that the decision was made, I had to choose a NoSQL database. I analysed all the popular ones but none fitted my requirements. I had one criteria for selecting a database: I must be able to code in Java. Most available systems were non-Java based which would be a significant issue for a one man project. Even if they had Java interface, the installation, setup, etc. were a tedious process. Having a database developed purely in Java has many advantages:
  1. Easy packaging with other applications
  2. Easy to install and run
  3. Can be embedded
  4. Can run in same or different VM
  5. Easy to debug
  6. Easy to test
After much searching, I came across OrientDB. Voila! That was what I needed! Going through its features only reinforced by belief in it. It is a pure Java solution and very small in size (500KB! 2MB since last release) . It can be embedded as well as deployed in networked mode. It is both schema-less and schema-based! This topped the feature list. It also supports SQL as a query language (Hmm, need to call NoSQL by some other name).

Some of the features taken from its homepage:

  1. SQL
  2. Super fast
  3. Transactional
  4. GraphDB
  5. Web ready (HTTP, REST, JSON)
  6. Everywhere (Pure Java)
  7. Extremely light
  8. Apache License (Here's the money)

End of architecture choice 1 for Greenscape.
Next decision on application framework pending.

Saturday, February 19, 2011

Grow Up Java! Proposing Java Desktop Edition

No, this is not another 'Java is Dead' rant. Java is very much alive and kicking. It is one of the best development and runtime platform available. By far the most stable platform of those. And that might just be its biggest bane. Ridiculous! How is stability a bane? You might ask. Well, you can see the slow progress in path breaking innovation, due to it. See the debate on closures and the progress on module system/OSGI.

The Java platform is growing only by a few APIs and small language changes/additions every year. There is no major big innovation happening. All the major changes that has been happening are outside it. Be it the  Dependency Injection, Persistence API, Scripting API, Desktop API, Application Framework API, they were only added to the platform subsequently. But they are just APIs. What we need is something big. Something that is 10 years advanced than today.

What I feel is Java is advancing in a bottom-up fashion. Small and slow changes at a time. What we need is a top-down approach. Think futuristic, design it and then only implement it in a bottom-up approach.

Take the example of new Swing Application Framework (now dropped from the standard). I think this new framework is a poorly envisioned idea. Look at the poor state of JMF. No substantial progress can be made on the desktop without support for multimedia. Yes, JavaFX is coming soon (for 3+ years ?). It is a big step forward but is not a standard (yet. Will it be ever?).

We have the JavaSE and JavaEE is built on top of it. Why can't we do the same with the desktop? Why can't we have something like the Java Desktop Edition or JavaDE ? Put all the big stuffs here. In this way, we can keep the JavaSE simple. Language and VM changes goes into JavaSE. Common APIs applicable to both EE and 'DE' goes into SE. But desktop additions like application framework, multimedia, browsing capabilities, gadget bar?, JavaFX, etc. goes into 'DE'. Of course, JavaME also needs to be considered but since industry's mood is to bring SE to ME, they can be considered the same for the sake of this blog.

Some might argue that the desktop is becoming irrelevant with the popularity of web applications and powerful browsers (with HTML 5). But as any unbiased report will say, the irrelevance of desktop will not happen anytime soon. After all, the browser needs to run on a desktop!

Basically, Java Desktop Edition becomes the new Operating System. It should have transparent integration with the underlying OS. Have functionalities in the form of services and modules on the lines of OSGI. So everything is a service: print service, document service, file service, graphic service, database service, < add your favorite services here >. So advanced capabilities can be exposed as a service to any module or applications (collection of modules) in the system. Imagine having Eclipse plugins as services exposed to all applications.

Think of the innovative possibilities of a Java Desktop (heard it before?):
- Java Browser with built-in support for applets, JavaFX - we can have applet plugins
- Browser can be embedded in any application
- GIMP like editor - JIMP?
- Standard installer for all applications - No OS specific versions of Eclipse or Netbeans (Java WAS supposed to be Write Once, Run Everywhere)
- Finally, a Java media player
- Desktop gadget bar
- Java Office
- < your favorite application here >

It is time for Java to grow up and act its age.

Thursday, January 27, 2011

Liferay (s)mashup - Google Maps


In the previous blog, we saw how to fire events asynchronously on the client side. In this blog, we will see the integration of Google Maps in a portlet.
Showing Google Maps in a portlet is a breeze. For this, create a portlet called 'Visual View'. This portlet displays the Google Map for visual interaction. It receives the source and destination from the Plan Your Travel portlet and shows the road route in the map.

Using Google Maps API

Google Maps API is a collection of web services providing geographic data for creating maps applications. These web services use HTTP requests to specific URLs, passing URL parameters as arguments to the services. Generally, these services return data in the HTTP request as either JSON or XML for parsing and/or processing by an application.  Let's get straight to the code.

Code Listing

<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%>
<portlet:defineObjects />
<style type="text/css">
#map_canvas {
    height: 500px;
    width: 400px
}
</style>

<script type="text/javascript">
    var <portlet:namespace />map;
    var <portlet:namespace />geocoder;
    var <portlet:namespace />directionDisplay;
    var <portlet:namespace />directionsService;
    var <portlet:namespace />rendererOptions = {
        draggable : true //make the map points draggable
    };
    var <portlet:namespace />initialLocation;
    var <portlet:namespace />infowindow;

    function <portlet:namespace />initialize() {
        var myLatlng = new google.maps.LatLng(-34.397, 150.644);
        var myOptions = {
            zoom : 8,
            center : myLatlng,
            mapTypeId : google.maps.MapTypeId.ROADMAP
        };

        <portlet:namespace />map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
        <portlet:namespace />geocoder = new google.maps.Geocoder();
        <portlet:namespace />directionsService = new google.maps.DirectionsService();
        <portlet:namespace />directionsDisplay = new google.maps.DirectionsRenderer(
                                                    <portlet:namespace />rendererOptions);
        <portlet:namespace />directionsDisplay.setMap(<portlet:namespace />map);
        <portlet:namespace />infowindow = new google.maps.InfoWindow();

        //geo location
        // Try W3C Geolocation method (Preferred)
        if (navigator.geolocation) {
            browserSupportFlag = true;
            navigator.geolocation.getCurrentPosition(function(position) {
                <portlet:namespace />initialLocation = new google.maps.LatLng(
                        position.coords.latitude, position.coords.longitude);
                contentString = "Location found using W3C standard";
                <portlet:namespace />map
                        .setCenter(<portlet:namespace />initialLocation);
                var marker = new google.maps.Marker({
                    map : <portlet:namespace />map,
                    position : <portlet:namespace />initialLocation
                });
            }, function() {
                <portlet:namespace />handleNoGeolocation(browserSupportFlag);
            });
        } else if (google.gears) {
            // Try Google Gears Geolocation
            browserSupportFlag = true;

            var geo = google.gears.factory.create('beta.geolocation');
            geo.getCurrentPosition(function(position) {
                <portlet:namespace />initialLocation = new google.maps.LatLng(
                        position.latitude, position.longitude);
                contentString = "Location found using Google Gears";
                <portlet:namespace />map.setCenter(<portlet:namespace />initialLocation);
                <portlet:namespace />infowindow.setContent(contentString);
                <portlet:namespace />infowindow.setPosition(<portlet:namespace />initialLocation);
                <portlet:namespace />infowindow.open(<portlet:namespace />map);
            }, function() {
                <portlet:namespace />handleNoGeolocation(browserSupportFlag);
            });
        } else {
            // Browser doesn't support Geolocation
            browserSupportFlag = false;
            <portlet:namespace />handleNoGeolocation(browserSupportFlag);
        }

        //add listener for directions change
        google.maps.event.addListener(
            <portlet:namespace />directionsDisplay,
            'directions_changed',
            function() {
                var leg = <portlet:namespace />directionsDisplay.directions.routes[0].legs[0];
                Liferay.fire('directionsChanged', {
                        origin : leg.start_address,
                        destination : leg.end_address
                });
            });
    }

    function <portlet:namespace />handleNoGeolocation(errorFlag) {
        if (errorFlag == true) {
            <portlet:namespace />initialLocation = newyork;
            contentString = "Error: The Geolocation service failed.";
        } else {
            <portlet:namespace />initialLocation = siberia;
            contentString = "Error: Your browser doesn't support geolocation. Are you in Siberia?";
        }
        <portlet:namespace />map.setCenter(<portlet:namespace />initialLocation);
        <portlet:namespace />infowindow.setContent(contentString);
        <portlet:namespace />infowindow.setPosition(<portlet:namespace />initialLocation);
        <portlet:namespace />infowindow.open(<portlet:namespace />map);
    }
 
    function <portlet:namespace />loadScript() {
        var script = document.createElement("script");
        script.type = "text/javascript";
        script.src = "http://maps.google.com/maps/api/js?sensor=false&callback=<portlet:namespace />initialize";
        document.body.appendChild(script);
    }

    window.onload = <portlet:namespace />loadScript;

    function <portlet:namespace />showDirection(source, destination) {
        var request = {
            origin : source,
            destination : destination,
            travelMode : google.maps.DirectionsTravelMode.DRIVING
        };
        <portlet:namespace />directionsService.route(request, function(
                response, status) {
            if (status == google.maps.DirectionsStatus.OK) {
                <portlet:namespace />directionsDisplay.setDirections(response);
            }
        });
    }

    Liferay.on('planTravel', function(event) {
        <portlet:namespace />showDirection(event.origin, event.destination);
    });
</script>

<div id="map_canvas"></div>

The Google Maps API requires a single div element with an id in the html page to be rendered. Here, we have given the id as 'map_canvas'. The loadScript() method dynamically loads the maps script file. This speeds up the page display. The initialize() method loads the map view with pre-configured values. Also, latest versions of most browsers support geolocation. Geolocation allows tracking user's current location. This information is used to load the map with user's current location.
Note the listener for the directions changed event from the map in the initialize function. When the directions changed event occurs, we republish the event using Liferay's event manager to notify other portlets. Other portlets receives this event to update their display. We saw this in the search form in the first part of this blog series.
To use the two portlets that we have developed so far, type in the names of cities in the origin and destination and hit the Search button. The road rout between the two cities will be shown in the Google map. Now, try changing the cities by dragging the marker in the map. The cities' names are updated in the Plan Your Travel portlet. We will see interesting applications of this event model in the next few portlets.
For more details working on the Google Maps API, see this and this.

Wednesday, December 29, 2010

Liferay (s)mashup - The Beginning


Part Two - Liferay (s)mashup - Google Maps

There are many popular web services available online. From the rich set of APIs from Google and Yahoo to social sites likeTwitter and Facebook, almost all service providers make available their services in the form of an open API. These rich APIs has given rise to new kind of applications called mashups. They are basically an integration of various web APIs from different providers put together to form new kind of applications.

In this and the next series of blogs, I will show how to integrate with some of these APIs.

To make meaningful use of these APIs, let me propose a sample application where these services can be used. Let us develop a travel planning site that helps user to plan their travel dates and location. Travelling is just not about location and date. There are other factors like weather conditions, transport availabiity, political turmoil, other events clashing with date. Questions such as 'Are any of my friends travelling on that date or to that location?', 'Do I know anyone from that location I am going to?' frequently comes up. Our site will try to resolve these questions so that a probable date and location can be zeroed on.

Features of the Application
 - Display input form for receiving the source, destination, journey date and duration.
 - Show the road route of the origin and destination in Google map
 - Show weather information of origin and destination
 - Show the event happening in origin and destination on and around the journey date
 - Show news related to origin and destination
 - Moving the marker on the Google map must also update all other information in real time like weather, events, news and  the input form.
 - More services as we go along

For the first requirement, create a new portlet. In the view.jsp file, design a basic form  for the user to fill in: Origin, Destination, Journey Date and Duration.

Code Listing

<form action="<liferay-portlet:actionURL portletConfiguration="true" />" method="post" name="<portlet:namespace />fm">
    <aui:input name="origin" label="Origin" />
    <aui:input name="destination" label="Destination" />
    <aui:input name="journeyDate" label="Journey Date" />
    <aui:input name="duration" label="Duration" suffix="(in days)" />
    <input type="button" value="Search" onclick="<portlet:namespace />postPlanDetails()" />
</form>

The journeyDate is shown as a calendar when user clicks on the input field. Define it in javascript as:

AUI().ready('aui-calendar', function(A) {
    var calendar1 = new A.Calendar({
        trigger : '#<portlet:namespace />journeyDate',
        dateFormat : '%d/%m/%Y',
        setValue : true,
        selectMultipleDates : true,
        on : {
            select : function(event) {
                var normal = event.date.normal;
                var detailed = event.date.detailed;
                var formatted = event.date.formatted;
            }
        }
}).render();

A.on('mousedown', function() {
    A.CalendarManager.hideAll();
}, document);

});

The postPlanDetails() javascript function is defined as follows:

function <portlet:namespace />postPlanDetails() {
    var origin = document.<portlet:namespace />fm.<portlet:namespace />origin.value;
    var dest = document.<portlet:namespace />fm.<portlet:namespace />destination.value;
    var journeyDate = document.<portlet:namespace />fm.<portlet:namespace />journeyDate.value;
    var duration = document.<portlet:namespace />fm.<portlet:namespace />duration.value;

    Liferay.fire('planTravel', {
        origin : origin,
        destination : dest,
        journeyDate : journeyDate,
        duration : duration
    });

}

The code in bold above functions publishes the 4 parameters as an event using Liferay client side Inter Portlet Communication. For more details on IPC, see Portlet to Portlet Communication.

This portlet also receives the 'directionsChanged' event from the Google Maps portlet. We will discuss this in the next blog.

Liferay.on('directionsChanged',
    function(event) {
        document.<portlet:namespace />fm.<portlet:namespace />origin.value = event.origin;
        document.<portlet:namespace />fm.<portlet:namespace />destination.value = event.destination;
});

Friday, December 24, 2010

Speeding up page loading

Developing fast loading pages is a complex task. Some have even defined it as an art. Large companies have dedicated professionals or hire external consultants to audit their applications for speed and responsiveness.

Here I provide some simple tips to speed up page loading in Liferay. Liferay also provides some in built features for fast page loading. Whenever available, Liferay support will be shown for each tip.


Tip 1: Sharing script and css files across portlets

For each script and css file included in a page, the browser makes separate requests to fetch them. This takes time. If the same file is included many times, the browser only makes requests the first time and for subsequent access, it picks from the cache. Therefore, common javascript code should be placed in a single file and this file should be included in all portlets that need it.

To share scripts across portlets, use the <header-portlet-javascript> tag or <footer-portlet-javascript> tag in the liferay-portlet.xml file. Similarly, use <header-portlet-css> tag or the <footer-portlet-css> tag in the liferay-portlet.xml file.


Tip 2: Lazy loading of scripts

Sometimes, scripts are not immediately needed after page load but only after user takes some action. To prevent such scripts being loaded on page load, we can load in the background after the page has loaded. There are two ways for lazy loading. One, the script is added to the <head> tag and the other is to add it to the <body> tag.

Adding to head tag:
function loadScript() {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "";
    var head = document.getElementsByTagName("head")[0];
    head.appendChild(script);

}

window.onload = loadScript;


The function loadScript() above creates a dynamic script tag and adds it to the first head tag available. It registers this function to be called on load of the page.

Adding to body tag:

function loadScript() {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "";
    document.body.appendChild(script);
}

window.onload = loadScript;


The function loadScript() above creates a dynamic script tag and adds it to the body. It registers this function to be called on load of the page.


Tip 3: Minimize script, stylesheet and html output

Whitespace and comments in script file or html pages takes up bandwith and so slows down page load. While comments and whitespaces are necessary for development, in production they should be removed. It would be nice to not to change the source code for production while still be able to remove the comments and whitespaces. A simple technique for this would be to use server side comments in jsp files and not to use html or javascript comments. Whitespaces cannot be handled this way.
Liferay provides a minifier process that strips down both whitespaces and comments from script and css files. The minifier also shortens function and variable names to make the script even smaller. To enable the minifier, set the option in the portlet-ext.properties file.

com.liferay.portal.servlet.filters.minifier.MinifierFilter=true

Another option Liferay provides is to load packed (compressed) versions of Liferay scripts and css. Enable it using

javascript.fast.load=true

By default, both the options are true. Make sure, they are not false on production systems.


Tip 4: Specify image and table width and height

If the browser can immediately determine the height and/or width of images and tables, it will be able to display a web page without having to reflow the content. This not only speeds the display of the page but prevents annoying changes in a page's layout when the page completes loading. For this reason, height and width should be specified for images and tables, whenever possible.