Liferay Portal & OSGi

Company Blogs 3 settembre 2011 Da Ray Augé Staff

Over the past several months I've been working on integrating an OSGi framework into Liferay Portal. The reasons for doing so are varied but include solutions to app server/servlet container dependency, library version conflicts, runtime module lifecycle, advanced extensibility, and so on.

If you are not familliar with OSGi, I recommend visiting the OSGi web site, and in particular reading the Benefits of Using OSGi.

The first publicly available code of this integration is available as an OSGi branch on my fork of the liferay-portal project on github.

The url of the branch is: https://github.com/rotty3000/liferay-portal/tree/OSGi

I'd like to encourage anyone having an interest in this work to clone the branch and send me feature improvements, bug fixes, etc, via git pull request, or even to just send me comments here.

 

Things I've done to date:

  • I'm using Eclipse Equinox 3.7 (but it works with any 4.1 or greater implementation of OSGi with no code changes, I tested Knopflerfish 3.2.0 and it worked very well, I'm sure Felix would work equally well).
  • Published all the services and beans of the portal's spring context as services in the framework so they can be used by bundles.
  • Developed a management portlet available in the control panel where the portal administor can add/remove/start/stop/update bundles, see all the bundle headers, all the service registrations and all service references of any bundle. (I'd like to eventually add bundle dependency graphing as well).
  • Make sure logging works (this can be tricky in OSGi due to libraries not due to OSGi).
  • Setup an auto deploy listener that will inspect any jar/war file dropped into the portal deploy folder that are OSGi bundles and automatically install them into the framework (and try to start them).
  • Setup a bridge Servlet which is wired into the OSGi HttpService to provide HttpServlet request dispatching for any servlets or jsps registered in the framework. (This feature depends on a bundle I created which I'm going to release in the comming days, in an OSGi branch of my fork of the liferay-plugins repository. The bundle depends on the eclipse equinox http servlet package which is available in the Equinox 3.7 zip.)
  • I've successfully tested various code modification scenarios such as replacing a service implementation and replacing a Util implementation, with all the virtues of OSGi bundle lifecycle goodness.
  • I've successfully deployed and used the recently released Gemini Blueprint implementation of the OSGi Blueprint specification (which you might call Spring for OSGi). It's a simple matter of deploying the 3 main bundles.
  • I've added BND lib to the portal so that it can be used for various OSGi packaging tasks.

Things I'd like to do next:

  • Make some changes to the Plugins SDK so that plugins can (optionally) be generated as OSGi bundles.
  • Support deploying the various bundle-ized plugins into the framework.
  • Support all the spring wiring of ServiceBuilder services found in plugins correctly.
  • Setup a portal packaging system so that we can bundle as set of bundles/plugins so there is no need to assemble and install them manually.
  • Modify the Liferay repository mechanism so that it supports bundles and implements either auto update or auto detectiong of possible updates for bundles in the repository (and also resolves/downloads dependencies).

 

Here are a couple of images of the OSGi Admin portlet.

List of bundles:

View of a Bundle:

 

I'll be presenting this work in a workshop at the upcoming West Coast Symposium in Anaheim on 21-22 of September.

I hope to hear your thoughs on this and maybe to discuss it with you at WCS.

Theme Settings and New Advanced Controls

Company Blogs 25 agosto 2011 Da Ray Augé Staff

To continue a recent string of theme related posts, I'm going to add another on the recently added feature allowing addition of advanced input controls and behaviors to theme settings.

Themes are the whip cream that bring flavour and sweetness to our portal fruit salads. Without beautiful and clever theme designs our fruit salad may taste healthy but can be bland and leave us wanting other treats.

To make sure this doesn't happen, Liferay tries its hardest, each generation, to bring new and/or improved features and richness to the theme APIs in order to empower designers to create master pieces.

One of the tools in the deisgners chest which was added in 6.0 and which added pretty significant flexibility to theme developers was the Settings API.

As the name implies, the Settings API is a mechanism that allows a designer to add configuration settings to their theme creations. These settings can then be used within the theme logic to do things like toggle features on or off, provide lists of options for display behaviors, provide boiler plate content fragments, etc. Using this API the designer can, with a single theme, attempt to provide a mixture of options that can potentially meet many more requirements and appeal to more users.

Using the Settings API

In order to use the settings API the designer must include a liferay-look-and-feel.xml file in their theme. The path should be:

<theme>/docroot/WEB-INF/liferay-look-and-feel.xml

As you might have guessed this is an xml file and a boiler plate file for 6.1 could be as trim as the following:

<?xml version="1.0"?>
 <!DOCTYPE
    look-and-feel PUBLIC
    "-//Liferay//DTD Look and Feel 6.1.0//EN"
    "http://www.liferay.com/dtd/liferay-look-and-feel_6_1_0.dtd">

<look-and-feel>
    <compatibility>
        <version>6.1.0+</version>
    </compatibility>
    <theme id="hotel" name="Hotel Theme" />
</look-and-feel>

Plainly enough this file defines the compatibility the theme has with different portal versions (in this case, since we're using features from 6.1 DTD, we're limiting our compatibility to 6.1+). It defines the theme element which contains the id of the theme as well as the name that will appear in the UI during theme selection/configuration.

In order to add settings we're going to add an element as a child of the <theme> element:

<?xml version="1.0"?>
 <!DOCTYPE
    look-and-feel PUBLIC
    "-//Liferay//DTD Look and Feel 6.1.0//EN"
    "http://www.liferay.com/dtd/liferay-look-and-feel_6_1_0.dtd">

<look-and-feel>
    <compatibility>
        <version>6.1.0+</version>
    </compatibility>
    <theme id="hotel" name="Hotel Theme" >
        <settings>
        </settings>
    </theme>
</look-and-feel>

Each setting is defined using a single <setting> elements with two main attributes, key (the name by which the setting is handled) and value (in the case of configurable setting, this will be the default value).

e.g.

...
<settings>
   <setting key="show-breadcrumb" value="true" />
</settings>
...

There are three optional attributes (added in 6.1):

  • configurable (whether or not the setting is static or editable from the UI)
  • type (the data type for the field: checkbox, select, text, or textarea, default is text if not provided)
  • options (in the case of type="select", options should contain a comma delimited list of values available to the selection)

Therefore a slightly more advanced setting might be:

...
<settings>
    <setting configurable="true" key="show-breadcrumb" type="checkbox" value="true" />
 </settings> 
...

So, how would this be used from the theme?

The usage is pretty simple and is handled by a single method on the ThemeDisplay object; getThemeSetting(String key). The ThemeDisplay object you say? Do not fear, this object is already in the context of both Velocity and Freemarker. In Velocity it is available as both $themeDisplay, as well as $theme_display. In Freemaker it's available as themeDisplay as well as theme_display.

Using the setting we created above, in your theme's portal_normal.vm (in a Velocity theme) you might do the following:

#set ($show_breadcrumb = $theme_display.getThemeSetting('show-breadcrumb'))
 
#if ($show_breadcrumb == 'true')
    <nav class="site-breadcrumbs" id="breadcrumbs">
        <h1>
            <span>#language("breadcrumbs")</span>
        </h1>
        #breadcrumbs()
    </nav>
#end

Checkbox type fields have string values of either "true" or "false".

Advanced controls

So far I have not talked about the Advanced Controls I referred to in the title of the post.

So you have controls, fine! Checkboxs, text fields, textareas, and selects are great but we can't exactly call this a great collection of "rich" controls. These are the lowest common controls that you'd probably expect. We're talking about theme developement here, so we want some POP and PIZZAZ (yes PIZZAZ!!!).

To solve this limitation without trying to provide for every scenario imaginable we added the ability for a setting definition to contain a block of javascript code which would be inlined immediately with the input field. This javascript would allow the designer to take the basic field types and turn them into a more advanced control.

For the following example I'm going to bind a color picker tool to a regular text field.

First the base xml for the definition:

...
<setting 
    configurable="true"
    key="portal-main-color"
    type="text"
    value="#EEF0F2">
<![CDATA[
]]>
</setting>
...

Now, I'm going to use the Alloy UI color-picker component and some other simple code for making the field read-only so users can't input faulty data:

...
<setting 
    configurable="true"
    key="portal-main-color"
    type="text"
    value="#EEF0F2">
<![CDATA[ 
    AUI().ready(
        'aui-color-picker-base',
        function(A) {
            var target = A.one('#[@NAMESPACE@]portal-main-color');
            target.attr('readonly', 'readonly');
            var currentValue = target.val();
 
            target.ancestor(
                '.aui-field-element').append(                 "<div id='[@NAMESPACE@]PortalMainColorPicker'></div>");
 
            setTimeout(function() {
                var [@NAMESPACE@]PortalMainColorPicker = new A.ColorPicker(
                    {
                        hex: currentValue
                    }
                ).render('#[@NAMESPACE@]PortalMainColorPicker');
 
                [@NAMESPACE@]PortalMainColorPicker.on(                     'colorChange',
                    function(event) {
                        var hex = [@NAMESPACE@]PortalMainColorPicker.get("hex");
 
                        target.val('#' + hex);
                    }
                );
            }, 0);
        }
    );
]]> 

</setting> ... 

If you read through the javascript you will see that we get an element using the A.one() alloy call. This is akin to document.getElementById() but it works by using css selectors. In this case we're getting the setting field by using the key of the field prefixed by the # symbol, which in CSS means that it's the ID of an element. The key is also prefixed by the token [@NAMESPACE@]. This token is used to avoid namespace collisions with other applications that may be floating around the page and is replaced at runtime with the actual namespace of the theme configuration app (in a portal we always need to be conscious that we may be co-existing with other apps). Use the token whenever creating public variables or dom objects.

Once we have the field, we can manipulate it however we like.

The Advanced Control Result

We've added the control, so what does it look like?

Here is the settings panel with the new field and the control:

With the control active:

Moving the cursor around the color-picker while left button pressing on the mouse will move the color-picker cursor and update the value in the setting text field until you release the button.

The Possibilities

Given the multitude of advanced controls we can create using javascript and the simple usage of the Settings API, the possibilities are quite limitless.

I hope that this takes your theme development to a whole new level of creativity and richness.

I'll be at WCS doing a couple of talks and a workshop which I'll be talking about shortly. So if you have questions regarding any of the things I've writen about recently, or about any other topic actually, and you're attending WCS, please bring those along with you.

See you at WCS!

Update: I wanted to add that the Theme Settings feature was originally implemented and donated to Liferay as part of a community contribution by José Ignacio Huertas (LPS-12468). I'd like to thank José for his contribution. This is a very good example of how a small contribution by the community can become a very cool feature. I would like to encourage others to participate by donating their feature implementations, suggestions, bug reports, documentation skills, or translation skills. This is the key way that Liferay has become the rich platform that it is.

Theme JSP Overrides

Company Blogs 22 agosto 2011 Da Ray Augé Staff

Ok, so in the last post I talked about JSP Include Buffer.

That post talked about how you'd override JSPs from Liferay's core in a maintainable way. Now I'd like to introduce you to a Liferay Theme feature, new in 6.1, that will grant you much more power when it comes to manipulating portal views from your theme using that plus this additional feature.

From your theme you can now include templates (following the same path maning as they are found in the core) in your themes which override those of the core, provided they have a template extension matching that of your themes. They will override the default view of the portatl but only where and when your theme is applied.

Now think about this! When you apply a theme onto any page of your portal, you can alter the view of any portlet on just those pages!

Let me illustrate with an example. I'll use the exact same example that was intriduced in the previous post just so it easier to follow along.

Assuming for the moment that our theme is a Freemarker theme, we add the following template to our theme:

<SDK_ROOT>/themes/my-theme/docroot/_diffs/templates/html/taglib/ui/page_iterator/start.ftl

In this template we place the following code (using the buffer pattern):

<#assign liferay_util = PortalJspTagLibs["/WEB-INF/tld/liferay-util.tld"] />

<@liferay_util["buffer"] var="html">
    <@liferay_util["include"] 
        page="/html/taglib/ui/page_iterator/start.jsp" 
        strict=true 
        useCustomPage=false 
    /> 
</@> 
 
<div style="border: 4px solid red; padding: 4px;">
    ${html}
</div>

So, you should note two things.

First, unlike in the previous post I don't have to use the .portal encoded name for the jsp because it's still where it started, the portal didn't relocate it since my override is in the theme (it only does that when using hook plugins).

Second, the attribute strict=true. As I mentioned before, but will again, this is to prevent the portal from doing a lookup for overrides when making the call to the underlying JSP (this is to avoid infinite recursion since we're in an override right now).

Ok, I can noew enhance or alter this view no problem without having to reproduce the whole of the original logic and I'm doing it from my theme no less!

What more could you ask for?

Well it turns out there IS more that could be asked and since it was asked for, we made it reality (that's what we do at Liferay, we make wishes come true).

The question was "Could you override a JSP, but only for a particular portlet?"

Since overriding a blog JSP only in blogs doesn't really make sense, nor does overriding a blog JSP only for message boards, the main case here is with taglib JSPs. Liferay has a vast number of tags and their view logic are largely implemented with JSPs.

So, the answer to that is, "Yes, now you can override a JSP in your theme for only specific portlets!"

How do you do it?

Simple! All you need to do is add the portlet Id of the target portlet, between the file name of the original JSP and the extension of the template like so:

<SDK_ROOT>/themes/my-theme/docroot/_diffs/templates/html/taglib/ui/page_iterator/start.19.ftl

Using a similar template containing (note the green border instead):

<#assign liferay_util = PortalJspTagLibs["/WEB-INF/tld/liferay-util.tld"] />

<@liferay_util["buffer"] var="html">
    <@liferay_util["include"]
        page="/html/taglib/ui/page_iterator/start.jsp"
        strict=true 
        useCustomPage=false 
    />
</@>

<div style="border: 4px solid green; padding: 4px;"> 
    ${html} 
</div>  

Now, you'll note that we have two new tempalate files in our theme:

_diffs/templates/html/taglib/ui/page_iterator/start.ftl
_diffs/templates/html/taglib/ui/page_iterator/start.19.ftl  

These two files do not conflict, and the precendence will be:

1. portlet specific template
2. non portlet specific template
3. (no file) default

If your template is trying to override the JSP for a portlet that is in a plugin, make sure to use the fully qualified portletId (which is encoded using the pattern portletid_WAR_plugincontextname).

Now if you place two portlets on the page while this theme is in effect on it, and each of those portlets use the liferay-util:page-iterator tag, and one of those portlets is the Message Boards Portlet (19) then you should see both red and green boxes around those parts of the page.

 

Oh, and remember that you CAN do the same from a Velocity theme:

_diffs/templates/html/taglib/ui/page_iterator/start.vm
_diffs/templates/html/taglib/ui/page_iterator/start.19.vm

The template (using the buffer pattern) would look more like this:

#set ($bufferTagClass = $portal.class.forName("com.liferay.taglib.util.BufferTag"))
#set ($includeTagClass = $portal.class.forName("com.liferay.taglib.util.IncludeTag"))
 
#set ($bufferTag = $bufferTagClass.newInstance())
#set ($V = $bufferTag.setPageContext($pageContext))
#set ($V = $bufferTag.setParent(null))
#set ($V = $bufferTag.setVar('html'))
 
#if ($bufferTag.doStartTag() == 2)
    #set ($V = $bufferTag.setBodyContent($pageContext.pushBody()))
    #set ($V = $bufferTag.doInitBody())
    #set ($includeTag = $includeTagClass.newInstance())
    #set ($V = $includeTag.setPageContext($pageContext))

    #set ($V = $includeTag.setPage('/html/taglib/ui/page_iterator/start.jsp'))

    #set ($V = $includeTag.setStrict(true))
    #set ($V = $includeTag.setUseCustomPage(false))
    #set ($V = $includeTag.runTag())
    #set ($V = $bufferTag.doAfterBody())
    #set ($V = $pageContext.popBody())
    #set ($V = $bufferTag.doEndTag())
#end
 
<div style="border: 4px solid red; padding: 4px;">
    ${pageContext.findAttribute('html')}
</div>

Now, I hope you find more uses for these features that colourful boxes. Good luck!

JSP Include Buffer

Company Blogs 22 agosto 2011 Da Ray Augé Staff

There are plenty of times when you might want to alter the default views of the portal. I'm not going to go into the why since that is hugely subjective. What I do want to show is the how you can do this in the most maintainable way possible.

The largest concern with core JSP modification in projects is of course maintainability. JSPs have no "extensability" feature of their own and so when taking modified core JSPs into your project you run the risk of causing yourself a tone of heartache during future upgrades.

JSPs don't have any contract that allows for cleanly identifying when something has changed or gone wrong between versions. At least with pure source code you can take a leap and just try compiling against the latest version and although you likely run into many errors, they are quite literally spelled out for you. With JSPs you don't have this luxery and it's quite possible to go weeks or months with JSPs that seem to work after an upgrade but that suddenly start to exhibit bad behavior as your system starts to hit those edge cases or outright fail for the same reason. This is typically because of some unforseen change in the core logic.

Developers often try to mitigate this by doing things like using some complex or just "obvious" comments explaining changed code, hoping they are still around to review the changes on future upgrades...

What's likely to happen is that there ends up being so many JSPs changed and/or that the same developer is not around for the next upgrade, a complete and thorough review of those changes simply doesn't get done, risks are run and the ensuing issues become costly.

So goes the story of "JSP Hell"!

Meanwhile JSPs are still the best performing RAD view techology in java, so they are still used quite heavily.

What to do?

Liferay has long experienced this scenario and though we knew that changing from JSP was either unlikely or unreasonable considering the alternatives, we still had to come up with a good solution to the problem.

The solution is quite simple, the JSP Include Buffer.

The basic principle is to simply buffer, then extend the result that is output from the original JSP. Thus we can change only the parts we care about, and latter if there is a change in the original, it will either cause our extension to fail, or it will be seamlessly integrated, exactly the way a source code change would (even though it won't be a build time error, it will still be far more evident than the previous alternative).

There is one limitation, the JSP that you want to perform this operation on must NOT be a jsp fragment (a fragement is a JSP that you include using the low level <%@ include file="****.jsp" %> directive.

Rather it should be one using an include tag, such as <jsp:include /> or in the case of Liferay <liferay-util:include />, i.e. one that is invoked through a request dispatcher.

The examples I'm going to show will be using the <liferay-uitl:include /> tag.

From a JSP Hook

Liferay offers a plugin type called a "Hook" that allows you to define JSPs that should override (not overwrite) JSPs in the core, and on deploy these jsps replace those in the core. BUT the core JSPs are not lost (remember override not overwrite). The core JSPs are simply re-located, thus they are still accessible.

The core JSPs are relocated to a new name which injects the keyword .portal between the file name and extension. Thus a core JSP having the name start.jsp after being "Hooked" would be located under the name start.portal.jsp (at the exact same path).

How does this help us with the above problem?

Simply it means that we have the original to work with and so we don't need to completely re-implement or reproduce the complete code in our "extended" implementation. We can still call the original, buffer it's output and then manipulate the result to our needs.

Here is a very simple example or wrapping the output of the original with a simple black box (you could do this via CSS but what's the fun in that, and this example could serve many more uses that simple css styling can ever achieve).

<%@ taglib uri="http://liferay.com/tld/util" prefix="liferay-util" %>

<liferay-util:buffer var="html">
    <liferay-util:include
        page="/html/taglib/ui/page_iterator/start.portal.jsp"
        useCustomPage="<%= false %>"
    />
</liferay-util:buffer>

<div style="border: 4px solid red; padding: 4px;">
    <%= html %>
</div>

From a FreeMarker Template

Can the same be achieved from Freemarker you ask? Certainly! And here is the same example as above.

 <#assign liferay_util = PortalJspTagLibs["/WEB-INF/tld/liferay-util.tld"] />

<@liferay_util["buffer"] var="html">
    <@liferay_util["include"]
        page="/html/taglib/ui/page_iterator/start.jsp"
        strict=true
        useCustomPage=false
    />
</@>

<div style="border: 4px solid red; padding: 4px;">
    ${html}
</div>

From a Velocity Template

Hold up! You're kidding right? Velocity can't do jsp tags...

Well it can! It's just not pretty. Why would you do this from a Velocity anyway? Well, you never know what people want to do in their themes and the next blog post I write about how to override JSPs from theme templates will make this here feature all the more interesting for theme developers, AND by supporting Velocity, won't force all those developers to convert their existing themes to Freemarker just so they can override the odd JSP.

So, here goes (remember I don't promise that this is beautiful, only that it works and works well).

#set ($bufferTagClass = $portal.class.forName("com.liferay.taglib.util.BufferTag"))
#set ($includeTagClass = $portal.class.forName("com.liferay.taglib.util.IncludeTag"))

#set ($bufferTag = $bufferTagClass.newInstance())
#set ($V = $bufferTag.setPageContext($pageContext))
#set ($V = $bufferTag.setParent(null))
#set ($V = $bufferTag.setVar('html'))

#if ($bufferTag.doStartTag() == 2)
    #set ($V = $bufferTag.setBodyContent($pageContext.pushBody()))
    #set ($V = $bufferTag.doInitBody())

    #set ($includeTag = $includeTagClass.newInstance())
    #set ($V = $includeTag.setPageContext($pageContext))

    #set ($V = $includeTag.setPage('/html/taglib/ui/page_iterator/start.jsp'))

    #set ($V = $includeTag.setStrict(true))
    #set ($V = $includeTag.setUseCustomPage(false))
    #set ($V = $includeTag.runTag())
    #set ($V = $bufferTag.doAfterBody())
    #set ($V = $pageContext.popBody())
    #set ($V = $bufferTag.doEndTag())
#end

<div style="border: 4px solid red; padding: 4px;">
    ${pageContext.findAttribute('html')}
</div>

And there you have it!

Now you'll notice that I bolded a coupled of lines above in the template examples. These lines, which set the field strict on the include tag to true, are VERY important. They tell the underlying tag NOT check if there are overrides for the specified JSP include (otherwise you may end up with infinite recursion and likely stack overflow errors ;) ).

 

I hope this can help some of you to biuld more maintainable solutions on Liferay with less fear of the upgrade process.

Opening a portlet in dialog from web content

Company Blogs 11 agosto 2011 Da Ray Augé Staff

Recently I was asked how to do this! Well, it's rather simple (providing you understand the limitations of accessing portlets without permission, and portlets that are not allowed to be loaded dynamically; a.k.a. add-default-resource, and so on).

Here goes!

First I created a simple web content structure/type with the following definition:

<root>
    <dynamic-element name='portlet-id' type='text' index-type='' repeatable='false'></dynamic-element>  
    <dynamic-element name='parameters' type='text' index-type='' repeatable='true'>         
        <dynamic-element name='value' type='text' index-type='' repeatable='false'></dynamic-element>
    </dynamic-element>
</root>

Then I created the following web content template:

 

<script type="text/javascript" charset="utf-8">
    AUI().ready('aui-dialog','aui-dialog-iframe','liferay-portlet-url', function(A) {
        var url = Liferay.PortletURL.createRenderURL();
        url.setPortletId("${portlet-id.data}");
        url.setWindowState('pop_up'); 

        #foreach ($parameter IN $parameters.getSiblings())
            url.setParameter("${parameter.data}", "${parameter.value.data}");
        #end  

        window.myDialog = new A.Dialog(
            {
                title: 'My Dialog',
                width: 640,
                centered: true
            }
        ).plug(
            A.Plugin.DialogIframe,
            {
                uri: url.toString(),
                iframeCssClass: 'dialog-iframe'
            }
        ).render();
    });
 </script>

 

As you can see there is a place for setting the portletId to craete an URL for, as well as a repeated section for assigning parameters to the url.

Creating an article with the Portlet Id value of 3 results in a dialog opening with the search portlet.

 

You can expand this simple example into whatever you like. The Alloy Dialog plugin is very rich and the Liferay.PortletURL library provides a complete API for creating all of the PortletURL types defined by the portlet spec.

 

Enjoy!

Secure RSS Feeds

Company Blogs 16 febbraio 2011 Da Ray Augé Staff

All secure RSS feeds now transparently support BASIC  Authentication.

The behavior is such that when you're logged in, the feeds will simply work as expected if you open them directly in the browser. If you log out, you'll suddenly be promted for BASIC  Authentication. If you use the url with an external RSS client and that client supports BASIC Authentication then simply give your credentials and you should be good to go.

Pleae note that BASIC Authentication transfers your passwords over the internet/network in plain text, so make sure that you have SSL enabled if you care about such things.

An alternative is also to enabled DIGEST Authentication (just add an init parameter for "digest_auth" in the filter declaration and restart).

Note of you're using an SSO of some kind, and the client making the request speaks in your SSO's toung, then of course you don't have to worry about any of this.

 

Enjoy!

(Update: The issue was resolved as of LPS-12308 r73243.)

Advanced Web Content Example with AJAX

Company Blogs 2 febbraio 2011 Da Ray Augé Staff

This example demonstrates several advanced features of Liferay's Web Content Management provided when taking full advantage of Liferay's Web Content Template processing engine. It'll demonstrate implementing PHASES of the portlet lifecycle, performing AJAX calls, using Alloy Javascript Library, using Liferay's SearchEngine, converting java objects to JSON for passing back as AJAX response body all from within our templates.

It starts with a structured piece of content we'll call a Widget, defined by the following Web Content Structure:

<root>
  <dynamic-element name='name' type='text' index-type='text' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[Enter plain text only.]]></entry>
			<entry name="label"><![CDATA[Widget Name]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
  <dynamic-element name='description' type='text_box' index-type='text' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[Prefer to use plain text here, at worst use only simple HTML tags like strong, em, etc.]]></entry>
			<entry name="label"><![CDATA[Description]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
  <dynamic-element name='image' type='document_library' index-type='keyword' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[]]></entry>
			<entry name="label"><![CDATA[Widget Image]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
  <dynamic-element name='document' type='document_library' index-type='keyword' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[Link a PDF document preferably.]]></entry>
			<entry name="label"><![CDATA[Widget Document]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
</root>

Make sure that each field of the structure is marked as "Searchable" as either text or token otherwise later on we won't be able to individually access those fields. The template for this structure might look like this (but it's not overly important for this example):

<h3>$name.data</h3>

<p><img style="float: left;" src="$image.data" alt="$name.data"/> $description.data</p>

<a href="$document.data">Spec Sheet</a>

Now given that we have a few pieces of content using this structure, our goal is to create an enhanced view that leverage AJAX in order to be cool... er fullfill our business requirements!

So, we're going to create a structure to act as the settings schema for a simple Web Content app:

<root>
  <dynamic-element name='number-of-items' type='text' index-type='' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[How many items to retrieve with ajax.]]></entry>
			<entry name="label"><![CDATA[Number of items]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
</root>

Finally, we get to the good part which is the template of the Web Content app. I've implemented this example using Velocity just because it's the one I'm most comfortable with. The template implements both the front end logic as well as the backend that will handle our AJAX request. Remember when writing templates that implement request handling to unckeck "Cacheable".

#set ($ns = $request.portlet-namespace)
#set ($companyId = $getterUtil.getLong($request.theme-display.company-id))
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))
#set ($numberOfItems = $getterUtil.getInteger($number-of-items.data))

#if ($request.lifecycle == 'RESOURCE_PHASE')

	## This phase will handle the ajax request.

#else

	## This phase (default is 'RENDER_PHASE') will handle the view.

#end

Let's look at the view logic and the AJAX call that drives it! It uses Liferay's very own Alloy Javascript Library to perform the AJAx call.

...
#else
	<table class="taglib-search-iterator">
		<thead>
			<tr class="portlet-section-header results-header">
				<th>
					Widgets
				</th>
			</tr>
		</thead>
		<tbody class="${ns}results-container">
			<tr class="portlet-section-body results-row last">
				<td>
					No Widgets
				</td>
			</tr>
		</tbody>
	</table>

	<script type="text/javascript">
	AUI().use(
		'aui-base', 'aui-io',
		function(A) {
			var search = function(eventType) {
				A.io.request(
					'${request.resource-url}',
					{
						dataType: 'json',
						on: {
							success: function(event, id, obj) {
								var instance = this;

								var hits = instance.get('responseData');
								
								var resultsContainer = A.one('.${ns}results-container');
						
								if (!hits && !hits.docs) {
									return;
								}

								resultsContainer.empty();
								
								for (var i = 0; i < hits.docs.length; i++) {
									var doc = hits.docs[i];

									console.log(doc);
								
									var title = doc.fields.map['web_content/name'].value || doc.fields.map.uid.value;
									var description = doc.fields.map['web_content/description'].value;
									var image = doc.fields.map['web_content/image'].value;
									var document = doc.fields.map['web_content/document'].value;
									
									var position = ' portlet-section-body';
									
									if (i % 2 == 1) {
										position = ' portlet-section-alternate alt';
									}
									
									if (i == 0) {
										position += ' first';
									}
									else if (i == hits.length - 1) {
										position += ' last';
									}
		
									resultsContainer.append(
										[
											'<tr class="results-row' + position + '">',
												'<td>',
													'<h3>',
														title,
													'</h3>',
													'<p>',
														'<img style="float: left;" src="',
															image,
															'" alt="',
															name,
															'"/>',
														//description,
													'</p>',
													'<a href="',
														document,
														'">Spec Sheet</a>',
												'</td>',
											'</tr>'
										].join('')
									);
								}
							}
						}
					}
				);
			}

			search();
		}
	);		
	</script>
#end

You'll notice that the bulk of the code lies in javascript processing the results into the table. I'm not the greatest of javascript developers so take my code with a grain of salt.

Also notice that the target of the AJAX call is a url generated from the request and is in fact one which invokes the current portlet in the "RESOURCE_PHASE". If you aren't familiar with portlets, the "RESOURCE_PHASE" is one which allows portlets to return output without the wrappings and trappings of the surrounding portal. Effectively the OutputStream or PrintWriter used by the portlet is not touched or altered in any way by the portal. This allows the portlet to do things like handle AJAX requests, or generally server any type of "static" resource, like images.

Finally, see how the value of each individual structure field is retrieved from the JSON object: doc.fields.map['web_content/name'].value. Each field that is marked as "Searchable" when creating the structure can be retrieved from the SearchEngine result prefixed by web_content/. The prefix exists so that dynamically created fields don't collide with fields of the actual Web Content Article object when indexed.

 

The final step is getting our content! To do that we'll invoke and query Liferay's SearchEngine from within the template and convert the results into JSON format which we will return as the response body of the request.

...
#if ($request.lifecycle == 'RESOURCE_PHASE')
	#set ($portalBeanLocator = $portal.getClass().forName('com.liferay.portal.kernel.bean.PortalBeanLocatorUtil'))

	#set ($searchEngine = $portalBeanLocator.locate('com.liferay.portal.kernel.search.SearchEngineUtil'))
	#set ($queryFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.search.BooleanQueryFactoryUtil'))
	#set ($sortFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.search.SortFactoryUtil'))
	#set ($jsonFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.json.JSONFactoryUtil'))

	#set ($fullQuery = $queryFactory.create())
	#set ($contextQuery = $queryFactory.create())
	
	#set ($V = $contextQuery.addRequiredTerm('companyId', $companyId))
	#set ($V = $contextQuery.addExactTerm('entryClassName', 'com.liferay.portlet.journal.model.JournalArticle'))
	#set ($V = $contextQuery.addRequiredTerm('groupId', $scopeGroupId))
	#set ($V = $contextQuery.addRequiredTerm('structureId', '10628'))
	#set ($V = $fullQuery.add($contextQuery, 'MUST'))
	
	#set ($sorts = $sortFactory.getDefaultSorts())
	
	#set ($hits = $searchEngine.search($companyId, $fullQuery, $sorts, 0, ))

	$jsonFactory.serialize($hits)
#else
...

What I've done here is create a SearchEngine query which limits the results to a specific structureId. This limitation is only imposed because I chose to limit the logic of the example to only support this one structure. It is entirely possible to query for arbitrary content types (even beyond just Web Content) as long as you are willing to implement the view logic to handle those.

Here's an image of what it might look like.

The complete template follows:

#set ($ns = $request.portlet-namespace)
#set ($companyId = $getterUtil.getLong($request.theme-display.company-id))
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))
#set ($numberOfItems = $getterUtil.getInteger($number-of-items.data))

#if ($request.lifecycle == 'RESOURCE_PHASE')
	#set ($portalBeanLocator = $portal.getClass().forName('com.liferay.portal.kernel.bean.PortalBeanLocatorUtil'))

	#set ($searchEngine = $portalBeanLocator.locate('com.liferay.portal.kernel.search.SearchEngineUtil'))
	#set ($queryFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.search.BooleanQueryFactoryUtil'))
	#set ($sortFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.search.SortFactoryUtil'))
	#set ($jsonFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.json.JSONFactoryUtil'))

	#set ($fullQuery = $queryFactory.create())
	#set ($contextQuery = $queryFactory.create())
	
	#set ($V = $contextQuery.addRequiredTerm('companyId', $companyId))
	#set ($V = $contextQuery.addExactTerm('entryClassName', 'com.liferay.portlet.journal.model.JournalArticle'))
	#set ($V = $contextQuery.addRequiredTerm('groupId', $scopeGroupId))
	#set ($V = $contextQuery.addRequiredTerm('structureId', '10628'))
	#set ($V = $fullQuery.add($contextQuery, 'MUST'))
	
	#set ($sorts = $sortFactory.getDefaultSorts())
	
	#set ($hits = $searchEngine.search($companyId, $fullQuery, $sorts, 0, $numberOfItems))

	$jsonFactory.serialize($hits)
#else
	<table class="taglib-search-iterator">
		<thead>
			<tr class="portlet-section-header results-header">
				<th>
					Widgets
				</th>
			</tr>
		</thead>
		<tbody class="${ns}results-container">
			<tr class="portlet-section-body results-row last">
				<td>
					No Widgets
				</td>
			</tr>
		</tbody>
	</table>

	<script type="text/javascript">
	AUI().use(
		'aui-base', 'aui-io',
		function(A) {
			var search = function(eventType) {
				A.io.request(
					'${request.resource-url}',
					{
						dataType: 'json',
						on: {
							success: function(event, id, obj) {
								var instance = this;

								var hits = instance.get('responseData');
								
								var resultsContainer = A.one('.${ns}results-container');
						
								if (!hits && !hits.docs) {
									return;
								}

								resultsContainer.empty();
								
								for (var i = 0; i < hits.docs.length; i++) {
									var doc = hits.docs[i];

									console.log(doc);
								
									var title = doc.fields.map['web_content/name'].value || doc.fields.map.uid.value;
									var description = doc.fields.map['web_content/description'].value;
									var image = doc.fields.map['web_content/image'].value;
									var document = doc.fields.map['web_content/document'].value;
									
									var position = ' portlet-section-body';
									
									if (i % 2 == 1) {
										position = ' portlet-section-alternate alt';
									}
									
									if (i == 0) {
										position += ' first';
									}
									else if (i == hits.length - 1) {
										position += ' last';
									}
		
									resultsContainer.append(
										[
											'<tr class="results-row' + position + '">',
												'<td>',
													'<h3>',
														title,
													'</h3>',
													'<p>',
														'<img style="float: left;" src="',
															image,
															'" alt="',
															name,
															'"/>',
														//description,
													'</p>',
													'<a href="',
														document,
														'">Spec Sheet</a>',
												'</td>',
											'</tr>'
										].join('')
									);
								}
							}
						}
					}
				);
			}

			search();
		}
	);		
	</script>
#end

Here it is again as XSLT!

<?xml version="1.0"?>

<xsl:stylesheet version="1.0"
	xmlns:BooleanQueryFactoryUtil="xalan://com.liferay.portal.kernel.search.BooleanQueryFactoryUtil"
	xmlns:JSONFactoryUtil="xalan://com.liferay.portal.kernel.json.JSONFactoryUtil"
	xmlns:BooleanQuery="xalan://com.liferay.portal.kernel.search.BooleanQuery"
	xmlns:SearchEngineUtil="xalan://com.liferay.portal.kernel.search.SearchEngineUtil"
	xmlns:SortFactoryUtil="xalan://com.liferay.portal.kernel.search.SortFactoryUtil"
	xmlns:str="http://exslt.org/strings"
	xmlns:xalan="http://xml.apache.org/xalan"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	exclude-result-prefixes="xalan"
	extension-element-prefixes="BooleanQueryFactoryUtil JSONFactoryUtil BooleanQuery SearchEngineUtil SortFactoryUtil str xalan">

	<xsl:output method="text" omit-xml-declaration="yes" />
	
	<xsl:param name="groupId" />

	<xsl:variable name="request" select="/root/request" />
	<xsl:variable name="ns" select="$request/portlet-namespace" />
	<xsl:variable name="companyId" select="$request/theme-display/company-id" />
	<xsl:variable name="scopeGroupId" select="$request/theme-display/scope-group-id" />
	<xsl:variable name="numberOfItems" select="/root/dynamic-element[@name='number-of-items']/dynamic-content" />

	<xsl:template name="out" match="@*|node()">
		<xsl:value-of select="local-name()"/><xsl:text> = </xsl:text>
		<xsl:copy>
			<xsl:apply-templates select="@*|node()"/>
		</xsl:copy>
	</xsl:template>

	<xsl:template match="/">
		<xsl:choose>
			<xsl:when test="$request/lifecycle = 'RESOURCE_PHASE'">
				<xsl:variable name="fullQuery" select="BooleanQueryFactoryUtil:create()" />
				<xsl:variable name="contextQuery" select="BooleanQueryFactoryUtil:create()" />
				<xsl:variable name="void1" select="BooleanQuery:addRequiredTerm($contextQuery, 'companyId', $companyId)" />
				<xsl:variable name="void2" select="BooleanQuery:addExactTerm($contextQuery, 'entryClassName', 'com.liferay.portlet.journal.model.JournalArticle')" />
				<xsl:variable name="void3" select="BooleanQuery:addRequiredTerm($contextQuery, 'groupId', $scopeGroupId)" />
				<xsl:variable name="void4" select="BooleanQuery:addRequiredTerm($contextQuery, 'structureId', '10628')" />
				<xsl:variable name="void5" select="BooleanQuery:add($fullQuery, $contextQuery, 'MUST')" />

				<xsl:variable name="sorts" select="SortFactoryUtil:getDefaultSorts()" />
				
				<xsl:message>
					<xsl:value-of select="$numberOfItems" />
				</xsl:message>
				
				<xsl:variable name="hits" select="SearchEngineUtil:search(number($companyId), $fullQuery, $sorts, number(0), number($numberOfItems))" />
				
				<xsl:value-of select="JSONFactoryUtil:serialize($hits)" />
			</xsl:when>
			<xsl:otherwise>
				<xsl:text disable-output-escaping="yes"><![CDATA[
					<table class="taglib-search-iterator">
						<thead>
							<tr class="portlet-section-header results-header">
								<th>
									Widgets
								</th>
							</tr>
						</thead>
						<tbody class="]]></xsl:text><xsl:value-of select="$ns" /><xsl:text disable-output-escaping="yes"><![CDATA[results-container">
							<tr class="portlet-section-body results-row last">
								<td>
									No Widgets
								</td>
							</tr>
						</tbody>
					</table>]]></xsl:text><xsl:text disable-output-escaping="yes"><![CDATA[
					<script type="text/javascript">
					AUI().use(
						'aui-base', 'aui-io',
						function(A) {
							var search = function(eventType) {
								A.io.request(
									']]></xsl:text><xsl:value-of disable-output-escaping="yes" select="$request/resource-url" /><xsl:text disable-output-escaping="yes"><![CDATA[',
									{
										dataType: 'json',
										on: {
											success: function(event, id, obj) {
												var instance = this;

												var hits = instance.get('responseData');
								
												var resultsContainer = A.one('.]]></xsl:text><xsl:value-of select="$ns" /><xsl:text disable-output-escaping="yes"><![CDATA[results-container');
						
												resultsContainer.empty();
								
												if (!hits && !hits.docs) {
													return;
												}

												for (var i = 0; i < hits.docs.length; i++) {
													var doc = hits.docs[i];

													var title = doc.fields.map['web_content/name'].value || doc.fields.map.uid.value;
													var description = doc.fields.map['web_content/description'].value;
													var image = doc.fields.map['web_content/image'].value;
													var document = doc.fields.map['web_content/document'].value;
									
													var position = ' portlet-section-body';
									
													if (i % 2 == 1) {
														position = ' portlet-section-alternate alt';
													}
									
													if (i == 0) {
														position += ' first';
													}
													else if (i == hits.length - 1) {
														position += ' last';
													}
		
													resultsContainer.append(
														[
															'<tr class="results-row' + position + '">',
																'<td>',
																	'<h3>',
																		title,
																	'</h3>',
																	'<p>',
																		'<img style="float: left;" src="',
																			image,
																			'" alt="',
																			name,
																			'"/>',
																		//description,
																	'</p>',
																	'<a href="',
																		document,
																		'">Spec Sheet</a>',
																'</td>',
															'</tr>'
														].join('')
													);
												}
											}
										}
									}
								);
							}

							search();
						}
					);		
					</script>]]></xsl:text>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>
</xsl:stylesheet>

Here's hoping someone finds this useful!

Liferay Expando MongoDB Hook Finished Review

Company Blogs 17 gennaio 2011 Da Ray Augé Staff

Last week I wrote about a hook we wrote for Expandos using MongoDB.

I just wanted to let you know that it just finished review and is ready for consumption.

It's been renamed to mongodb-hook in case we ever want to persist more services to it.

Have a look and enjoy!

Expandos III (Liferay, NoSQL, and MongoDB)

Company Blogs 8 gennaio 2011 Da Ray Augé Staff

Update: The expando-mongodb-hook plugin is now committed to SVN and available from trunk.

Over the past few months the hype around NoSQL type databases had really been heating up the tech news and blog feeds. There seems to be an overwhelming desire to find scallability solutions that don't seem to be addressed with an RDBMS. What is Liferay to do?

Could Liferay support some form of NoSQL integration? I think so, and I surely couldn't go long without doing something to draw attention to the fact that Liferay is a prime candidate as a viable platform for scalling dynamically via a NoSQL backend.

The most obvious way I could see to leverage a NoSQL solution was with perhaps the most dynamic aspect of the portal, Expandos (and by association Custom Fields).

In order prove the concept of NoSQL with Liferay we decided to write an adapter (using a Liferay Hook pattern) to build a backend for Expando on MongoDB. I had no real idea how long it would take to accomplish but we decided to try. As it turns out it was not too difficult. We now have a fully functional adapter to store all of Liferay's dynamic Expando data in a highly scalable MongoDB. But note that Expandos still support all Liferay permissions. And Custom Fields are still indexed along with the entities anywhere they would be normally. This is a fantastic demonstration of just how extensible Liferay portal really is.

I tested against the version of mongodb that was readily available for Ubuntu 10.04 (1:1.2.2-1ubuntu1.1). I also tried to make sure to support cluster configurations. So check out the portlet.properties file in the plugin as well as the mongodb driver javadocs for what and how to set that up.

I did several small usage tests (none of which were load testing, since this was an informal design) to see that everything was working the right way. I created several Custom Fields on several different entites and tested CRUD opperations to make sure that the data was landing (as well as being removed/updated) where I wanted it, in MongoDB.

Meanwhile, I was also using the Mongo DB command line client mongo to make sure that everything was working from that end. I added a custom field called test to the User entity, and for the first user in the system, I set the value to test value . Here is an example of what we see via mongo:

 

[rotty@rotty-desktop  expando-mongodb-hook]$ mongo
MongoDB shell version: 1.2.2
url: test
connecting to: test
type "exit" to exit
type "help" for help
> show dbs
admin
local
lportal_0
lportal_10135
> use lportal_10135
switched to db lportal_10135
> db.getCollectionNames()
[
	"com.liferay.portal.model.User#CUSTOM_FIELDS",
	"com.liferay.portlet.blogs.model.BlogsEntry#CUSTOM_FIELDS",
	"com.liferay.portlet.documentlibrary.model.DLFileEntry#CUSTOM_FIELDS",
	"system.indexes"
]
> db.getCollection("com.liferay.portal.model.User#CUSTOM_FIELDS").count()
1
> db.getCollection("com.liferay.portal.model.User#CUSTOM_FIELDS").find()
{ "_id" : ObjectId("4d28f318fcfcc08a7855ebe4"), "companyId" : 10135, "tableId" : 17205, "rowId" : 10173, "classNameId" : 10048, "classPK" : 10173, "valueId" : 17207, "test" : "test value" }
> 

So far so good! As you can see the data is landing nicely into the Mongo DB database.

 

While that was a good test I also wanted to make sure that other use cases would work just as well. I decided to revive the First Expando Bank example to see how that would work.

I first had to make a few small API changes in the Velocity template. The updated template is attached. See this article and the follow up for more information on that topic.

After adding some accounts into the First Expando Bank app, the mongo console results looked like this:

 

> db.getCollectionNames()
[
	"AccountsTable#AccountsTable",
	"com.liferay.portal.model.User#CUSTOM_FIELDS",
	"com.liferay.portlet.blogs.model.BlogsEntry#CUSTOM_FIELDS",
	"com.liferay.portlet.documentlibrary.model.DLFileEntry#CUSTOM_FIELDS",
	"system.indexes"
]
> db.getCollection("AccountsTable#AccountsTable").count()
3
> db.getCollection("AccountsTable#AccountsTable").find()
{ "_id" : ObjectId("4d29292abda2c08a05e35e67"), "companyId" : 10135, "tableId" : 17320, "rowId" : 1294543146642, "classNameId" : 17313, "classPK" : 1294543146642, "valueId" : 17336, "balance" : 55, "firstName" : "Ray", "lastName" : "Auge", "modifiedDate" : "Sat Jan 08 2011 22:19:06 GMT-0500 (EST)" }
{ "_id" : ObjectId("4d292945bda2c08a06e35e67"), "companyId" : 10135, "tableId" : 17320, "rowId" : 1294543173086, "classNameId" : 17313, "classPK" : 1294543173086, "valueId" : 17337, "balance" : 120, "firstName" : "Daffy", "lastName" : "Duck", "modifiedDate" : "Sat Jan 08 2011 22:19:33 GMT-0500 (EST)" }
{ "_id" : ObjectId("4d292958bda2c08a07e35e67"), "companyId" : 10135, "tableId" : 17320, "rowId" : 1294543192848, "classNameId" : 17313, "classPK" : 1294543192848, "valueId" : 17338, "balance" : 300, "firstName" : "Mickey", "lastName" : "Mouse", "modifiedDate" : "Sat Jan 08 2011 22:19:52 GMT-0500 (EST)" }
> 

Excelent! It would appear that all our use cases are covered from automatic Custom Fields via the UI to programmatic use in a CMS template.

I'd love to get your feedback about it! Please note that there is currently no rich way to perform queries (à la MongoDB). But with a little enginuity we could probably make that possible.

Liferay Nomination to the JCP

Company Blogs 25 ottobre 2010 Da Ray Augé Staff

The JCP Election is just around the corner and Liferay is on the ballot for the first time ever. Being that it is the first time, you may want to know what motivates Liferay's desire for a seat on the JCP?

Frankly it's not a political one. It's purely pragmatic! Liferay would like to see things get done, to see the JCP process in the most effective state it can be. Liferay is not new to the java world and Liferay knows open source. It knows it well as a provider as well as a consumer being itself one of the largest consumers of open source java projects in the industry. On the other hand Liferay is aware of the need to drive revenue whether as an individual or as an enterprise. So, we like to not waste time and get things done. There is no political engine at work. There is only reality which demands that solutions arrive in a timely fashion; and yet Liferay is keenly aware that standardization is nessecary and can take time. Adressing the needs of a wide audience can be difficult and filled with roadblocks beyond just the technical ones. To that end Liferay wants to bring it's pragmatic know how to bare and drive innovation at it's heart. There is no better venue than the JCP.

So then, the key techincal concerns for Liferay are primarly to preserve an open, unfragmented, ubiquitous Java platform, to see improved development agility, to strive for increased modularity and to reach those goals sooner than later. The future looks bright if the JCP's focus can stay on track and Liferay would like to help make that happen.

Custom Velocity Tools and Liferay 6.0

Company Blogs 4 ottobre 2010 Da Ray Augé Staff

A while back I wrote a post about adding custom tools to the Liferay Velocity context.

In 6.0 a change was made such that the behaviour has changed slightly. Now all such tools are plain old beans which must implement an interface.

The changes also means that I have a lot less code to write and less wiring to do. Let's see how we'd do it now using exactly the same tool as that old post.

The interface again:

package com.mytool;

public interface MyTool {

	public String operationOne();

	public String operationTwo(String name);

}

We need an implementation of the interface:

package com.mytool;

public class MyToolImpl implements MyTool {

	public String operationOne() {
		return "Hello out there!";
	}

	public String operationTwo(String name) {
		return "Hello " + name + "!";
	}

}

Our spring configuration only requires a single bean definition:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
	<bean id="com.mytool.MyTool" class="com.mytool.MyToolImpl" />
</beans>

Of course in order for our bean definitions to be read we need to make sure it gets loaded, so we'll name it so that it's found by the context loader WEB-INF/applicationContext.xml (We could have used the portal-esk spring config mechanism, but I wanted to demonstrate that Liferay is flexible and sensitive to existing coding behaviors.

I mentioned that we need a context loader, so there is one more change required. While before you could only add tools within a ServiceBuilder enabled plugin, that is no longer required. All you have to do is add the following context loader listener the your web.xml (but only if your plugin is not a portlet type plugin):

    <listener>
        <listener-class>com.liferay.portal.kernel.spring.context.PortletContextLoaderListener</listener-class>
    </listener>

This effectively tells the portal to create a BeanLocator object associated with your plugin and to read the beans definitions that were defined in WEB-INF/applicationContext.xml.

Now we're ready to use our tool in a Velocity template:

Since we've done this in a plugin, you will have to specify the 'contextPathName' of the plugin so that the appropriate BeanLocator can be used to lookup your tool. For example, the context path name of your plugin being "test-velotool-hook", then you'd use the following in your template:

#set ($myTool = $utilLocator.findUtil('test-velotool-hook', 'com.mytool.MyTool'))

$myTool.operationOne()

$myTool.operationTwo('Ray')

I've linked a source hook plugin that you can drop into your plugins SDK and deploy just by doing ant deploy.

[test-velotool-hook.zip]

Debugging Liferay in Eclipse

Company Blogs 22 settembre 2010 Da Ray Augé Staff

When should you use a debugger during development? At ALL times! At least that's my view.

Isn't that a hassle? Well debugging sure can be, but if you setup your environment in a specific way, it can actually become very fast and almost transaprent. I like transparent!

Here are steps I take to configure my tomcat (I've followed the same steps with JBoss):

  1. Note the deployment path of tomcat (in my case I'll use /bundles/tomcat-6.0.x)
  2. In Eclipse, open the "Run Configurations" manager (Run -> Run Configurations...)
  3. On the "Java Application" node, right click and choose "New".
  4. On the first tab (Main) select the project you are running in tomcat
  5. In the Main Class field enter org.apache.catalina.startup.Bootstrap
  6. On the Arguments tab, in the Program Arguments field enter start
  7. In the VM arguments field enter the following:

    -Xms1024m
    -Xmx1024m
    -XX:PermSize=128m
    -XX:MaxPermSize=256m
    -XX:+CMSClassUnloadingEnabled
    -Dorg.apache.catalina.loader.WebappClassLoader.ENABLE_CLEAR_REFERENCES=true
    -Dcatalina.base=/bundles/tomcat-6.0.x
    -Dcatalina.home=/bundles/tomcat-6.0.x
    -Djava.io.tmpdir=/bundles/tomcat-6.0.x/temp
    -Dexternal-properties=${workspace_loc:<NAME_OF_YOUR_ECLIPSE_PROJECT>}/portal-web/docroot/WEB-INF/src/portal-developer-tomcat.properties
    -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
    -Djava.util.logging.config.file=/bundles/tomcat-6.0.x/conf/logging.properties


    You many notice that I included a reference to my portal project's portal-developer-tomcat.properties file. This way I can change portal settings and only need to restart the portal without a redeploy, which is very convenient.

    You may also notice that I included tomcat's juli log manager so that all the normal web app configurations work as expected and in the Eclipse console, which is also very nice.

    Adjust your heap settings as desired, but the above settings have worked for me in 99.999% of cases.
     
  8. In the Working directory field choose "Other" and enter /bundles/tomcat-6.0.x/bin. Notice that this is the tomcat path, plus the /bin folder.
  9. The JRE tab should be already set properly, but you can choose any JRE that is installed that is 1.5+.
  10. On the Classpath tab, remove any entries under User Entries and then choose "Add External JARs...".
  11. Select the 3 jars located in /bundles/tomcat-6.0.x/bin.
  12. On the Source tab make sure that you have both Default, as well as your project in the list.

    Optionally here you can add any plugin projects that you are working on, as well as adding the path to the jsp java classes (typically /bundles/tomcat-6.0.x/work/Catalina/localhost/_)
  13. On the Common tag choose at least Debug and Run under "Display in favorites menu".
  14. Click "Apply" near the bottom of the dialog.
  15. Finally, to start the portal in debug mode, click the Debug menu in the Eclipse toolbar, and choose the run config you jsut created.

    You should see at this point output on the Eclipse console showing that tomcat is starting up.

I've been using this techinque for at least 3 years and I run the portal this way 99.999% of the time. By doing so, I can at any point in time add a breakpoint and start trouble shooting and I don't have to worry about re-connecting to or restarting the portal in debug mode, I know it's already done.

If you want to add any other java apps this way, the simples technique is to first run the app in the traditional way, then observe the process log to see what the full command that was actually used to start it, and from where. Once you know that, you can add any app to Eclipse Run Configuration.
 

 

Listing Article fragments dynamically using Liferay's WCM Templates

Company Blogs 16 settembre 2010 Da Ray Augé Staff

 So my last blog post involved a classloader trick for velocity but it also involved a problem of dynamically listing article fragments on a page.

Now, there are a few ways to do this, but the two that come to mind are:

1) Render each article using a specified template which only shows the content of the desired fields (fine, but seems overkill)

2) Parse the article content XML to get only specified bits (also fine, but there are concerns when the content is localized, not to mention isn't the XML handling code ugly/heavy?)

I've mostly taken approach 1), but could approach 2) be clean and light if we do it right? I think it can!

Here is how I did it (using the trick from the last post of course ):

#set ($journalArticleLocalService = $serviceLocator.findService('com.liferay.portlet.journal.service.JournalArticleLocalService'))
#set ($localeTransformer = $portal.getClass().forName('com.liferay.portlet.journal.util.LocaleTransformerListener').newInstance())
#set ($VOID = $localeTransformer.setLanguageId($request.theme-display.language-id))

#set ($companyId = $getterUtil.getLong($request.theme-display.company-id))
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))

#set ($obc = $portal.getClass().forName("com.liferay.portlet.journal.util.comparator.ArticleDisplayDateComparator").newInstance())

#set ($articles = $journalArticleLocalService.search($companyId, $scopeGroupId, '', null, null, null, null, null, null, 0, null, -1, -1, $obc))

<ul> 
#foreach ($article in $articles)
     #set ($xml = $localeTransformer.onXml($article.content))
     #set ($doc = $saxReaderUtil.read($xml))
     #set ($title = $doc.valueOf("//dynamic-element[@name='title']/dynamic-content/text()"))
     <li>${title}</li>
#end
</ul>

 

Now that's contrived, but it's pretty lean, mean and straight to the point, which is always good.

Tricks of Velocity class loading

Company Blogs 16 settembre 2010 Da Ray Augé Staff

Recently I was asked to solve a WCM dynamic Template problem which involved listing articles using the business logic of the template.

One of the first problems I encountered was of course doing a search for the Articles via the JournalArticleLocalService which, in the case of the searchs which return List<JournalArticle>, require an OrderByComparator. Since there is no way to directly instantiate classes in Velocity I wondered how I was going to handle this issue.

Luckily my colleague Thiago was working with a community member (Sabrina Schürhaus Locks) who had already solved the problem in a rather interesting and simple way:

 

#set ($obc = $portal.getClass().forName("com.liferay.portlet.journal.util.comparator.ArticleDisplayDateComparator").newInstance())

 

It's so simple I feel silly that I never thought of this myself.

Kudos Sabrina!

Writing Liferay Plugins with Groovy

Company Blogs 24 luglio 2010 Da Ray Augé Staff

We've been saying for a long time that you could write plugins in a variety of languages, but we never really had any examples to prove that.

Of course some languages will be far simpler to achieve this that others. It'll be far simpler to do this using languages that have native java bindings. Groovy is writen natively in java and even extends the JDK with awesome new features, so that's the one I'm choosing.

Another concern is speed of delievery while not comprimising on performance, stability, ability to debug, etc.

Groovy has awesome tooling support under Eclipse, and even takes part in debugging with little problem at all. All I had to do was install the Groovy Eclipse plugin and add the Groovy nature to my plugin project (yes, I'm using the LIDE. But since this isn't a blog about LIDE, I'm not gonna waste time showing that. It's safe to say that it wasn't a huge effort to add the Groovy support to the .project file and the IDE came in handy when specifying the props configurations which were few).

So, I'm going to demonstrate how to write a hook in Groovy. I want to demonstate 2 things in particular:

  1. That you can indeed write Liferay plugins using another language, in this case Groovy.
  2. That you can use the power of a wonderful language like Groovy to achieve things that would take an order of magnitude more code if you were to try with java.

I also have three goals:

  1. I'm going to implement a single listener of as many model events as I choose, listenting to as many model types as I choose, without having to write more that a single implementation.
  2. I'm going to persist audit events to the DB using as little persistence code as physically possible.
  3. I'm going to do it really fast (I'm pretending my boss wanted this done last week...).

So, where do I start?

The first thing I need to do is support compiling my groovy code. To do that I'm going to override the default ant target which normally compiles the plugin code, but only for this one plugin (Cause I don't want to harm other projects until I have this nailed down to a science.)

  • First thing to do is download the latest Groovy jar and add that to your plugin's <project>/docroot/WEB-INF/lib folder (I used groovy-all-1.7.3.jar, which was the latest version at the time of writting).
     
  • Next, I'll open up <project>/build.xml and paste the following ant target definition:
                <target name="compile">
                    <antcall target="merge" />
                
                    <mkdir dir="docroot/WEB-INF/classes" />
                    <mkdir dir="docroot/WEB-INF/lib" />
                
                    <copy todir="docroot/WEB-INF/lib">
                        <fileset dir="${app.server.lib.portal.dir}" includes="${plugin.jars}" />
                    </copy>
                
                    <copy todir="docroot/WEB-INF/tld">
                        <fileset dir="${app.server.portal.dir}/WEB-INF/tld" includes="${plugin.tlds}" />
                    </copy>
                
                    <if>
                        <available file="docroot/WEB-INF/src" />
                        <then>
                            <if>
                                <available file="tmp" />
                                <then>
                                    <path id="plugin-lib.classpath">
                                        <fileset dir="docroot/WEB-INF/lib" includes="*.jar" />
                                        <fileset dir="tmp/WEB-INF/lib" includes="*.jar" />
                                        <pathelement location="docroot/WEB-INF/classes" />
                                        <pathelement location="tmp/WEB-INF/classes" />
                                    </path>
                                </then>
                                <else>
                                    <path id="plugin-lib.classpath">
                                        <fileset dir="docroot/WEB-INF/lib" includes="*.jar" />
                                        <pathelement location="docroot/WEB-INF/classes" />
                                    </path>
                                </else>
                            </if>
                
                            <copy todir="docroot/WEB-INF/lib">
                                <fileset dir="${app.server.lib.portal.dir}" includes="${required.portal.jars}" />
                            </copy>
                
                            <if>
                                <available file="docroot/WEB-INF/lib/portal-impl.jar" />
                                <then>
                                    <fail>
                .
                
                Detected inclusion of portal-impl.jar in WEB-INF/lib.
                
                portal-impl.jar is designed with a large number of singleton classes which are
                instantiated on the basis that they will exist alone in the application server.
                
                While compile time issues may be resolved, portlets cannot be made to work by
                simply adding portal-impl.jar, because doing so violates the above assumption,
                and the resulting problems will be extremely difficult to debug.
                
                Please find a solution that does not require portal-impl.jar.
                                        </fail>
                                </then>
                            </if>
                
                            <taskdef name="groovyc"
                    classname="org.codehaus.groovy.ant.Groovyc"
                    classpathref="plugin.classpath"/>

                <groovyc
                    classpathref="plugin.classpath"
                    destdir="docroot/WEB-INF/classes"
                    srcdir="docroot/WEB-INF/src"
                >
                    <javac
                        compiler="${javac.compiler}"
                        debug="${javac.debug}"
                        deprecation="${javac.deprecation}"
                        fork="${javac.fork}"
                        memoryMaximumSize="${javac.memoryMaximumSize}"
                        nowarn="${javac.nowarn}"
                    />
                </groovyc>
            </then>     </if>     <antcall target="merge" /> </target>

     
    Note the part in red that replaces the default compiler call with one to groovyc (Don't worry, this will also compile any java code seamlessly if there is some in the project).
     

  • Now I make sure that my hook has a properties file defined cause I'm going to write an application Startup event, and a model listener:
    <hook>
        <portal-properties>portal.properties</portal-properties>
    </hook>
     
  • My two implementations are going to be called com.liferay.sample.groovy.GStartupAction and com.liferay.sample.groovy.GModelListener so I'll set those up in the props file (for now I'm just picking a whole bunch of interesting models to listen to, I can tune this again later):
    application.startup.events=com.liferay.sample.groovy.GStartupAction

    value.object.listener.com.liferay.portal.model.User=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portal.model.Layout=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.blogs.model.BlogsEntry=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.blogs.model.BlogsStatsUser=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.journal.model.JournalArticle=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.journal.model.JournalStructure=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.journal.model.JournalTemplate=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.messageboards.model.MBCategory=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.messageboards.model.MBMessage=com.liferay.sample.groovy.GModelListener
    value.object.listener.com.liferay.portlet.messageboards.model.MBThread=com.liferay.sample.groovy.GModelListener
     
  • Ok, so now we have all the house keeping stuff done, and I can get to work on writting the code. For the sake of speed, I'm using the simplest type of DB interaction that comes in the form of Groovy's groovy.sql.Sql class which easily lets me do JDBC operations using very lean amount of code (my DB operations are on MySQL specifically, again for the sake of speed, I'm not concerned with syntax that will work with other DBs).

    The first thing I need to do is create the DB table, if it doesn't exist. I'm going to do that in my GStartupAction:
    package com.liferay.sample.groovy

    import com.liferay.portal.kernel.events.SimpleAction
    import com.liferay.portal.kernel.util.InfrastructureUtil

    import groovy.sql.Sql

    class GStartupAction extends SimpleAction {

        public void run(String[] arg0) {
            try {
                _sql.rows('select count(*) from AuditLog')
            }
            catch (e) {
                _sql.execute '''
                    create table AuditLog (
                        auditId bigint not null primary key,
                        groupId bigint,
                        className varchar(75),
                        classPK bigint,
                        classUuid varchar(75),
                        auditDate datetime,
                        description varchar(200),
                        model longtext
                    )
                '''
            }
        }

        private static final Sql _sql = new Sql(InfrastructureUtil.dataSource)

    }
     You gotta admit that's pretty short. That's it for that class.
     
  •  On to GModelListener. Note that we want it to work with any of the models we throw at it. And we also want to limit the code, so we're going to extend com.liferay.portal.model.BaseModelListener<T>. The only events we care about are: onBeforeCreate, onBeforeRemove, onBeforeUpdate.
    package com.liferay.sample.groovy

    import com.liferay.portal.model.BaseModelListener

    class GModelListener extends BaseModelListener {

        void onBeforeCreate(model) {
        }

        void onBeforeRemove(model) {
        }

        void onBeforeUpdate(model) {
        }

    }
    We start with the above.
     
  • I need a couple other objects setup, logging, and the Groovy Sql object that I need to do the DB operations:
    package com.liferay.sample.groovy

    import com.liferay.portal.kernel.log.LogFactoryUtil
    import com.liferay.portal.kernel.util.InfrastructureUtil;
    import com.liferay.portal.model.BaseModelListener

    class GModelListener extends BaseModelListener {

        void onBeforeCreate(model) {
        }

        void onBeforeRemove(model) {
        }

        void onBeforeUpdate(model) {
        }

        private static final _log = LogFactoryUtil.getLog(GModelListener.class)

        private static final _sql = new Sql(InfrastructureUtil.dataSource)

        private static final _auditLog = _sql.dataSet("AuditLog")

    }

    The _auditLog variable is a Groovy groovy.sql.DataSet object that lets me do really clean operations on a given table.
     
  • Finally, I want to write a closure that will do the work of updating the AuditLog table, but first I want a very simple API for it:
    package com.liferay.sample.groovy

    import com.liferay.portal.kernel.log.LogFactoryUtil
    import com.liferay.portal.kernel.util.InfrastructureUtil;
    import com.liferay.portal.model.BaseModelListener

    class GModelListener extends BaseModelListener {

        void onBeforeCreate(model) {
            audit(model, "onBeforeCreate")
        }

        void onBeforeRemove(model) {
            audit(model, "onBeforeRemove")
        }

        void onBeforeUpdate(model) {
            audit(model, "onBeforeUpdate")
        }

        def audit = { model, message ->
            // do my work here
        }

        private static final _log = LogFactoryUtil.getLog(GModelListener.class)

        private static final _sql = new Sql(InfrastructureUtil.dataSource)

        private static final _auditLog = _sql.dataSet("AuditLog")

    }

    That's pretty straight forward! It'll do the trick.
     
  • Now, I want to track who did the particular operation, so I need to current user's Id, so it can be recoreded. The simplest way to do this is to get the current PermissionChecker and if it exists, then record the userId:

        def audit = { model, message ->
            def userId = PermissionThreadLocal.permissionChecker?.userId
        }

     
  • We also want the Group, anbd for the sake of completeness, if the model has a uuid field we want that too:

        def audit = { model, message ->

            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

        }


    But, how do we handle with the models without those fields? Isn't there a bunch of reflection involed in handling things like that? Well, in java, Yes! In Groovy, No! This is so easy in Groovy it's almost trivial:

        def audit = { model, message ->

            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

            if (model.metaClass.respondsTo(model, 'getGroupId')) {
                groupId = model.groupId
            }

            if (model.metaClass.respondsTo(model, 'getUuid')) {
                uuid = model.uuid
            }
        }


    That's to say, if the model object has a given method, we'll just call it. If not, well we just ignore it and keep the default value.
  • Next we'll populate a map of data we want to store in the AuditLog table:

        def audit = { model, message ->

            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

            if (model.metaClass.respondsTo(model, 'getGroupId')) {
                groupId = model.groupId
            }

            if (model.metaClass.respondsTo(model, 'getUuid')) {
                uuid = model.uuid
            }

            def map = [
                auditId: CounterLocalServiceUtil.increment(),
                groupId: groupId,
                className: model.class.name,
                classPK: model.primaryKey,
                classUuid: uuid,
                auditDate: new Date(),
                description: message + " by " + String.valueOf(userId),
                model: model.toString()
            ]

        }


    Simple enough!
     
  • Now, let's store and log the result:

        def audit = { model, message ->

            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

            if (model.metaClass.respondsTo(model, 'getGroupId')) {
                groupId = model.groupId
            }

            if (model.metaClass.respondsTo(model, 'getUuid')) {
                uuid = model.uuid
            }

            def map = [
                auditId: CounterLocalServiceUtil.increment(),
                groupId: groupId,
                className: model.class.name,
                classPK: model.primaryKey,
                classUuid: uuid,
                auditDate: new Date(),
                description: message + " by " + String.valueOf(userId),
                model: model.toString()
            ]

            _auditLog.add(
                auditId: map.auditId, groupId: map.groupId,
                className: map.className, classPK: map.classPK,
                classUuid: map.classUuid, auditDate: map.auditDate,
                description: map.description, model: map.model)

            if (_log.infoEnabled) {
                _log.info map
            }
        }


    Phew!!! We're done. We got it all done before lunch time.

    Arguably, we could have saved another 13 lines od code if we didn't need the map that we used for both persisting and logging the event. We could just as easily passed the raw values to the _auditLog instance, but wait.. we don't want the code to be TOO short, otherwise our "lines of code contributed" factor will make it look like we never do ANY work at all!!!
     
  • The whole class looks like this:
    package com.liferay.sample.groovy

    import com.liferay.counter.service.CounterLocalServiceUtil
    import com.liferay.portal.kernel.log.LogFactoryUtil
    import com.liferay.portal.kernel.util.InfrastructureUtil;
    import com.liferay.portal.model.BaseModelListener
    import com.liferay.portal.security.permission.PermissionThreadLocal;

    import groovy.sql.Sql

    class GModelListener extends BaseModelListener {

        void onBeforeCreate(model) {
            audit(model, "onBeforeCreate")
        }

        void onBeforeRemove(model) {
            audit(model, "onBeforeRemove")
        }

        void onBeforeUpdate(model) {
            audit(model, "onBeforeUpdate")
        }

        def audit = { model, message ->
            def userId = PermissionThreadLocal.permissionChecker?.userId

            def groupId = 0
            def uuid = ''

            if (model.metaClass.respondsTo(model, 'getGroupId')) {
                groupId = model.groupId
            }

            if (model.metaClass.respondsTo(model, 'getUuid')) {
                uuid = model.uuid
            }

            def map = [
                auditId: CounterLocalServiceUtil.increment(),
                groupId: groupId,
                className: model.class.name,
                classPK: model.primaryKey,
                classUuid: uuid,
                auditDate: new Date(),
                description: message + " by " + String.valueOf(userId),
                model: model.toString()
            ]

            _auditLog.add(
                auditId: map.auditId, groupId: map.groupId,
                className: map.className, classPK: map.classPK,
                classUuid: map.classUuid, auditDate: map.auditDate,
                description: map.description, model: map.model)

            if (_log.infoEnabled) {
                _log.info map
            }
        }

        private static final _log = LogFactoryUtil.getLog(GModelListener.class)

        private static final _sql = new Sql(InfrastructureUtil.dataSource)

        private static final _auditLog = _sql.dataSet("AuditLog")

    }

    Now you can go back and add the missing models and you're good to go.

    Oh, and when you're done, take the rest of the day off... you deserve it!

Liferay Staging 6.0

Company Blogs 15 luglio 2010 Da Ray Augé Staff

Some of the latest details about Liferay Staging in 6.0 are now on the Wiki: Staging - 6.0

  • Local Live
  • Remote Live
  • Better Differential Publishing
  • Stored connections settings for remote staging (as you'd expect)
  • Un-Staged Portlets (pick and choose which portlets are staged)
  • Lots and Lots of fixes and usability improvements

Let us know what you think about it.

Ideas for Blog and WCS talks

Company Blogs 22 giugno 2010 Da Ray Augé Staff

I haven't been blogging as much as I'd like...

My return to core after a long year+ on a project hasn't turned out quite the way I had envisioned.  (Don't get me wrong, I'm SOOOO happy to be back; although, to be fair I was with an awesome client.)

I had two particular things in mind when coming back:

1) lots of new code to play with
2) lots of blogging about all things Liferay

Part 1) "lots of new code to play with" has turned out as I expected, and more. So much code....   The sad thing is that it also has gotten in the way of 2) "lots of blogging about all things Liferay".

 

 

Really, there are so many new features and refinements in Liferay 6.0 that it's mind boggling to think of how much innovation has taken place, and I missed so much of it. I frankly sometimes don't know where to start.

In fact, I was so mind boggled the weeks after being back that during lead up to ECS I got so confused over what and who was doing what that I ended up not having a single talk about code. And to be honest, I should never speak publicly about anything except code. Thankfully Brett saved me and did a talk in my place. (Thanks Brett! I know it wasn't all a win, win for you.)

Anyhow, I promise that WCS will be a different story. I will speak about nothing but code. In fact, I will actually speak IN code in order to make up for the missed opportunity at ECS . (Just kidding!)

No but honestly, I want to know what would be the most compelling topic(s) for me to discuss at WCS. I have several ideas; some may turn up as blog posts, but some could be specially for WCS.

I'm still planning to write about groovy|ruby plugins in the near future. I could always talk about the latest way to RAD on Liferay (which is always of interest to me personally).

But hey, if you're planning to be at WCS, and you have a topic that you'd like me to talk about, throw it up here and I'll see if we can work it in.

Whatever the topic, I can try to speak a little on it whether I do a full blog on it or not (that is, unless I know who can give a better answer), so don't hold back (too much)!

 

Ideas?

Debuging Ant classpath issues

Company Blogs 29 maggio 2010 Da Ray Augé Staff

When working with Liferay, sometimes you might encounter classpath issues during ant builds.

I have a little trick which may help.

It's a small bit of xml that you can add to a target which dumps a classpath by refid:

 

 <property name="debug.classpath" refid="service.classpath"/>
            
 <for list="${debug.classpath}" delimiter=":" param="cur">
     <sequential>
         <echo>@{cur}</echo>
     </sequential>
 </for>

 

That's it! Short and sweet.

Setting tabwidths to match the project settings in GIT and VI

Company Blogs 19 maggio 2010 Da Ray Augé Staff

I use Git and VI(m) quite a bit, and in the Liferay project the source format rules define that leading whitespace character is tab (with potentially trainling spaces before the rest of the line in some cases, but that's another story).

Furthermore, tab width is 4.

Since I'm commonly in the command line I hate that the source doesn't represent the same way that I see it in the IDE, especially when reviewing diffs since most console output has tab width of 8.

GIT

So the question is "How do I make git output use tab width of 4 instead of the default 8?"

After searching long without success, I finnally learned that git uses the "pager" paradigm for displaying diffs, and that the external pager can be tuned in the ~/.gitconfig file.

Awesome!

So, to adjust the tab width add the following to your config:

[core]
    pager = less -FXRS -x4

 

This tells git to use "less" command as the external pager, then it passes some settings to less, but most importantly it sends the setting:

-x<n>

which sets the tab width.

VI(m)

Now to get the same result in VI(m) you can edit the ~/.vimrc file. Coolness!

set tabstop=4
set ts=4
set shiftwidth=4
set autoindent

 

Great! That's it.

Now all my important console output has the proper tab widths (and I can tell if I messed up my bchan style formatting).  

;)

Git the most out of your bash prompt

Company Blogs 14 maggio 2010 Da Ray Augé Staff

I was looking for a way to add as much relevant info about my Liferay git repository to my prompt as would not slow it down to much, cause I hated having to constantly do git branch/status to find branch/state all the time.

So after reading a couple nice posts about it,

http://henrik.nyh.se/2008/12/git-dirty-prompt

http://plasti.cx/2009/10/23/vebose-git-dirty-prompt

I took the best of both of those (performance wise and output wise), and since I use git-svn added to mine the requirement of showing the svn revision my branch is currently synced with.

Here is the bash code I added to by ~/.bashrc :

function parse_git_dirty {
status=`git status 2> /dev/null`
        dirty=`    echo -n "${status}" 2> /dev/null | grep -q "Changed but not updated" 2> /dev/null; echo "$?"`
        untracked=`echo -n "${status}" 2> /dev/null | grep -q "Untracked files" 2> /dev/null; echo "$?"`
        ahead=`    echo -n "${status}" 2> /dev/null | grep -q "Your branch is ahead of" 2> /dev/null; echo "$?"`
        newfile=`  echo -n "${status}" 2> /dev/null | grep -q "new file:" 2> /dev/null; echo "$?"`
        renamed=`  echo -n "${status}" 2> /dev/null | grep -q "renamed:" 2> /dev/null; echo "$?"`
        bits=''
        if [ "${dirty}" == "0" ]; then
                bits="${bits}☭"
        fi
        if [ "${untracked}" == "0" ]; then
                bits="${bits}?"
        fi
        if [ "${newfile}" == "0" ]; then
                bits="${bits}*"
        fi
        if [ "${ahead}" == "0" ]; then
                bits="${bits}+"
        fi
        if [ "${renamed}" == "0" ]; then
                bits="${bits}>"
        fi
        echo "${bits}"
}

function parse_git_svn_revision {
        ref1=$(__git_ps1 | sed -e "s/ (\(.*\))/(git: \1$(parse_git_dirty))/")
        #ref1=$(parse_git_branch)

        if [ "x$ref1" != "x"  ]; then
                ref2=$(git svn info | grep Revision)
                echo " ${ref1} (svn: r"${ref2#Revision: }") "
        fi
}

PS1='\[\033[0;37m\][\[\033[0;31m\]\u@\h\[\033[0;33m\]`parse_git_svn_revision`\[\033[0;32m\]\W\[\033[0;37m\]]\$ '

 
This is what it looks like:

the output of a bash session using the new prompt

 

As per Plasticx's blog the dirty flags are as such:

  • ‘☭’ – files have been modified
  • ‘?’ – there are untracted files in the project
  • ‘*’ – a new file has been add to the project but not committed
  • ‘+’ – the local project is ahead of the remote
  • ‘>’ – file has been moved or renamed

 

Much better!

Risultati 21 - 40 su 79.
Items 20
di 4