Writing Liferay Apps with Web Content Templates

One of the often overlooked features of Liferay's WCM system is the ability to write non-trivial apps using it.  There have been a few blog posts about this, notably Ray Augé's Advanced Web Content Example With AJAX.  In the community, it's great for me because I can quickly create interesting visualizations of community data and share it with you immediately.  There are some pros and cons to this approach:

Benefits:

  • No compilation needed - WCM relies on the use of Templates, written in interpreted (i.e. scripted) languages such as Velocity Templates.  This means you can quickly make a change and see your results quickly.
  • No deployment needed - since Web Content isn't a java portlet, you don't need to re-deploy.  More importantly it means you don't have to wait for a website administrator to deploy it if you cannot deploy yourself!
  • You can combine presentation (e.g. HTML/JS/CSS) and logic (e.g. Velocity) into the same template, keeping related code together.

Drawbacks:

  • Velocity is first and foremost a templating/presentation language.  It is not a general purpose computing language, so die hard MVC types will probably dismiss the use of Velocity in this way and call me a heretic/lunatic.  It's great for prototyping though!  
  • Currently, Structures and Templates aren't versioned, and they do not participate in Liferay's Workflow system.  So you can't revert to older (working) versions of templates if you make a mistake.
  • No compilation needed - so it's not as fast as the native bytecode that would result from the equivalent java source code.  But it's still quite fast.
  • Velocity and other scripting languages have weird quirks that often cannot be caught except through trial and error, and limitations (e.g. no use of generics) that compiled/strongly typed languages have.
  • You can combine presentation (e.g. HTML) and logic (e.g. Velocity) into the same template :)

In my opinion, Liferay WCM is a very good solution for app prototyping or for non-trivial apps that don't have tons of logic or page flows in them.  You have already seen an example of this in the Community Activity Map, and the example I use below forms the basis for the Hot Topics app that you can now see on liferay.org.

Basic Template Template

To get started creating an app of this nature, you need to start with simple Web Content Template that is itself a template:

#if ($request.lifecycle == "RENDER_PHASE")

  ## This phase will handle the presentation code (i.e. HTML/CSS/JS).  Any calls
  ## to the ${request.resource-url} will retrieve the result of evaluating the below
  ## RESOURCE_PHASE below.

#elseif ($request.lifecycle == "RESOURCE_PHASE")

  ## This phase will handle the AJAX request like a portlet's serveResource() method

#end

So decide what needs to be executed on the server side, and put it in the RESOURCE_PHASE.  This is typically where most if not all of the business (i.e. non-presentation) logic goes.  Put the presentation logic in the RENDER_PHASE.

Hot Topics Example

For this app, I want to show which threads have the most posts in the last week.  So, I needed to query Liferay's Message Boards.  Since there is no getMostActiveThreadsInTheLastWeek() method (I know.. what's up with that??), I needed a custom query.  This means using Liferay's DynamicQuery feature.  But from Velocity?  Turns out it's not that bad.  Here's the full RESOURCE_PHASE code to create and execute a Dynamic Query, and generate a JSON object as a result which contains the most active threads in the last week:

#set ($portletNamespace = $request.portlet-namespace)
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))

#if ($request.lifecycle == "RENDER_PHASE")

  ## bunch of display logic to show the JSON result nicely

#elseif ($request.lifecycle == "RESOURCE_PHASE")
  #set ($logFactory = $portal.getClass().forName('com.liferay.portal.kernel.log.LogFactoryUtil'))
  #set ($log = $logFactory.getLog('mylog'))

  #set ($portalBeanLocator = $portal.getClass().forName("com.liferay.portal.kernel.bean.PortalBeanLocatorUtil"))
  #set ($jsonFactory = $portalBeanLocator.locate("com.liferay.portal.kernel.json.JSONFactoryUtil"))
  #set ($mbMessageLocalService = $portalBeanLocator.locate("com.liferay.portlet.messageboards.service.MBMessageLocalService.velocity"))
  #set ($mbThreadLocalService = $portalBeanLocator.locate("com.liferay.portlet.messageboards.service.MBThreadLocalService.velocity"))

  #set ($calClass = $portal.getClass().forName("java.util.GregorianCalendar"))
  #set ($mbMessageClass = $portal.getClass().forName("com.liferay.portlet.messageboards.model.MBMessage"))
  #set ($mbThreadClass = $portal.getClass().forName("com.liferay.portlet.messageboards.model.MBThread"))
  #set ($dqfu = $portal.getClass().forName("com.liferay.portal.kernel.dao.orm.DynamicQueryFactoryUtil"))
  #set ($pfu = $portal.getClass().forName("com.liferay.portal.kernel.dao.orm.ProjectionFactoryUtil"))
  #set ($ofu = $portal.getClass().forName("com.liferay.portal.kernel.dao.orm.OrderFactoryUtil"))

  #set ($now = $calClass.getInstance())
  #set ($weeksago = $calClass.getInstance())
  #set ($prevweeks = 0 - $getterUtil.getInteger($period.data))
  #set ($V = $weeksago.add(3, $prevweeks))


  #set ($q = $dqfu.forClass($mbThreadClass))
  #set ($rfu = $portal.getClass().forName("com.liferay.portal.kernel.dao.orm.RestrictionsFactoryUtil"))

  #set ($groupIdCriteria = $rfu.ne("categoryId", $getterUtil.getLong("-1")))
  #set ($V = $q.add($groupIdCriteria))

  #set ($groupIdCriteria = $rfu.eq("groupId", $getterUtil.getLong($scopeGroupId)))
  #set ($V = $q.add($groupIdCriteria))

  #set ($companyIdCriteria = $rfu.eq("companyId", $getterUtil.getLong($companyId)))
  #set ($V = $q.add($companyIdCriteria))

  #set ($statusCriteria = $rfu.eq("status", 0))
  #set ($V = $q.add($statusCriteria))

  #set ($lastPostDateCriteria = $rfu.between("lastPostDate", $weeksago.getTime(), $now.getTime()))
  #set ($V = $q.add($lastPostDateCriteria))

  #set ($V = $q.setProjection($pfu.property("threadId")))

  #set ($res1 = $mbMessageLocalService.dynamicQuery($q))
  #set ($q2 = $dqfu.forClass($mbMessageClass))

  #set ($inCriteria = $rfu.in("threadId", $res1))
  #set ($V = $q2.add($inCriteria))

  #set ($createDateCriteria = $rfu.between("createDate", $weeksago.getTime(), $now.getTime()))
  #set ($V = $q2.add($createDateCriteria))

  #set ($V = $q2.setProjection($pfu.projectionList().add($pfu.groupProperty("rootMessageId")).add($pfu.alias($pfu.rowCount(), "msgCount"))))
  #set ($V = $q2.addOrder($ofu.desc("msgCount")))
  #set ($V = $q2.setLimit(0, 7))

  #set ($res2 = $mbMessageLocalService.dynamicQuery($q2))

  #set ($jsonArray = $jsonFactory.createJSONArray())

  #foreach ($msgSum in $res2)

    #set ($rootMsgId = $msgSum.get(0))
    #set ($msgCount = $msgSum.get(1))
    #set ($subject = $mbMessageLocalService.getMessage($rootMsgId).getSubject())

    #set ($jsonObject = $jsonFactory.createJSONObject())
    #set ($V = $jsonObject.put("subject", $stringUtil.shorten($htmlUtil.escape($subject), 55)))
    #set ($V = $jsonObject.put("msgid", $rootMsgId))
    #set ($V = $jsonObject.put("msgCount", $msgCount))
    #set ($V = $jsonArray.put($jsonObject))
  #end
{
"jsonArray": $jsonArray
}
#end



Details

There are many things going on here:

Velocity Debugging/Logging

  #set ($logFactory = $portal.getClass().forName('com.liferay.portal.kernel.log.LogFactoryUtil'))
  #set ($log = $logFactory.getLog('mylog'))
This gives me a way to debug the code by looking at the server log (if you are using this kind of app so that you can bypass your website admin, chances are you won't have access to the server logs, so this won't help you).  To emit debug info, I can do things like $log.error($msgCount) or $log.error("Hi There").
 

Creating references for arbitrary JVM classes

  #set ($calClass = $portal.getClass().forName("java.util.GregorianCalendar"))
This allows me to create references to any class known in the JVM for doing things like calling static methods, etc.  Many of these are needed for constructing Dynamic Queries.
 

Calculating Now and a Week Ago

  #set ($now = $calClass.getInstance())
  #set ($weeksago = $calClass.getInstance())
  #set ($prevweeks = 0 - $getterUtil.getInteger($period.data))
  #set ($V = $weeksago.add(3, $prevweeks))
This creates Calendar objects representing the current time, and a week ago.  Note that the number of weeks is specified in a web content structure using the period structure element.
 
The rest of the code constructs two dynamic queries:
  • The first one ($q) queries for MBThread entities that have a categoryId of -1 (MBThreads that do not have a categoryId of -1 are not threads from the message boards portlet, instead they are threads for things like comments on document library entries, etc).  The query also includes other criteria, like groupId/companyId must match the "current" groupId/companyId of the site in which the web content is placed, the status must be 0 (indicating it is an approved (i.e. not draft or deleted) entry), and most importantly the lastPostDate must be between my desired time period start and end.  Finally, I am not interested in all of the MBThread entity - I just need the threadId.  So my query includes a Projection that only returns the threadId.
  • The second query ($q2) queries for all MBMessage entities that match my new critieria: they must have a threadId of one of the threads identified in the first query (hence the in criteria), and the message's createDate must also be between my start/end dates.  This is to avoid counting messages in the thread that occured before the cutoff dates.  Finally, this gem:
  #set ($V = $q2.setProjection($pfu.projectionList().add($pfu.groupProperty("rootMessageId")).add($pfu.alias($pfu.rowCount(), "msgCount"))))
  #set ($V = $q2.addOrder($ofu.desc("msgCount")))
  #set ($V = $q2.setLimit(0, 7))
This creates a projection that is grouped by the rootMessageId, since each message in the same thread will have the same rootMessageId (which I eventually use to construct the URL to the message), and includes a count of the messages that match (with an alias defined so I can refer to the row count when specifying the order of the results via addOrder()).  I also limit the results to 7 because I don't want to show any more than that (this is a simple app).  This second query returns a result table that looks like:
 
rootMessageId rowCount (alias="msgCount")
10232 (the id of the first message in the thread) 22 (the number of MBMessages with this rootMessageId in the date range)
11542 21
12323 18
...and so on ... and so on (descending order)

So after the Dynamic Queries executes, it's just a matter of constructing a JSONObject (and sanitizing/sizing the actual text of the subject of the thread) and returning it.

Liferay WCM and Velocity Gotchas

Velocity is first and foremost a templating/presentation language.  It is not a general purpose computing language, so die hard MVC types will probably dismiss the use of Velocity in this way and call me a heretic/lunatic.  But it also means that some things are hard (or impossible, for example did anyone catch that I hard-coded Calendar.MONTH to be 3 ?  You can't reference static member variables of a class unless it is already part of the Velocity context in which a template is evaluated).  There are many other perils awaiting the adventurous Velocity coder.  I learned many things through trial and error (and the help of my IRC friends on the #liferay channel!).  Here are some more:
 
  • Don't forget to use $var instead of var.  If you forget the $, you won't get syntax errors, just silent errors and half of your code will next execute.
  • If you can use an intelligent IDE (like IntelliJ IDEA or Eclipse) and its Velocity syntax checking, do it!  I saved tons of time by using IntelliJ and declaring variable types, which allowed for autocompletion and type checking.  For example, I had tons of these:
#* @vtlvariable name="request" type="java.util.Map" *#
#* @vtlvariable name="httpUtil" type="com.liferay.portal.kernel.util.HttpUtil" *#
#* @vtlvariable name="htmlUtil" type="com.liferay.portal.kernel.util.HtmlUtil" *#
#* @vtlvariable name="obc" type="com.liferay.portal.util.comparator.UserLastNameComparator" *#
#* @vtlvariable name="serviceLocator" type="com.liferay.portal.velocity.ServiceLocator" *#
#* @vtlvariable name="teamLocalService" type="com.liferay.portal.service.TeamLocalServiceUtil" *#
#* @vtlvariable name="mbMessageLocalService" type="com.liferay.portlet.messageboards.service.MBMessageLocalServiceUtil" *#
#* @vtlvariable name="mbThreadLocalService" type="com.liferay.portlet.messageboards.service.MBThreadLocalServiceUtil" *#
  • Any time you access things from the ${request.theme-display}, or access one of your WCM structure fields that represent a number (but are of type "Text" in the template), they are probably not of the the type that you want.  You need to use generous amounts of $getterUtil.getXXX calls to make sure.  For example, 
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))
will work (and makes  $scopeGroupId a Long), whereas
#set ($scopeGroupId = $request.theme-display.scope-group-id)
Will results in a $scopeGroupId that is not a Long, and so if you pass it in to a method that is expecting a Long, it won't work, and will probably silently fail and you'll be befuddled.
  • If you want to create a new instance of a class (e.g. a HashMap) you can'y say new HashMap().  Velocity does not know what "new" is - after all, you're using a presentation/templating language, not Java!   But we're using it for more than display.  So as a workaround you can do things like $portal.getClass().forName("java.util.HashMap").newInstance().
  • Accessing elements of an array cannot be done using $array[0].  You have to use $array.get(0).
 

The Full Source to Hot Topics

Here's the full source, including my display code, and my IntelliJ variable declarations (which may have some unnecessary declarations, but I use this block for other stuff too).  If you spot errors or poor coding technique or whatever else, please let me know so I can learn!
#* @vtlvariable name="portletNamespace" type="java.lang.String" *#
#* @vtlvariable name="portal" type="com.liferay.portal.util.Portal" *#
#* @vtlvariable name="getterUtil" type="com.liferay.portal.kernel.util.GetterUtil" *#
#* @vtlvariable name="stringUtil" type="com.liferay.portal.kernel.util.StringUtil" *#
#* @vtlvariable name="max-members" type="com.liferay.portlet.journal.util.TemplateNode" *#
#* @vtlvariable name="team-name" type="com.liferay.portlet.journal.util.TemplateNode" *#
#* @vtlvariable name="section-members" type="com.liferay.portlet.journal.util.TemplateNode" *#
#* @vtlvariable name="groupId" type="java.lang.String" *#
#* @vtlvariable name="sectionMembers" type="java.lang.String" *#
#* @vtlvariable name="locale" type="java.util.Locale" *#
#* @vtlvariable name="companyId" type="java.lang.String" *#
#* @vtlvariable name="scopeGroupId" type="java.lang.String" *#
#* @vtlvariable name="sectionName" type="java.lang.String" *#
#* @vtlvariable name="section-name" type="com.liferay.portlet.journal.util.TemplateNode" *#
#* @vtlvariable name="params" type="java.util.LinkedHashMap" *#
#* @vtlvariable name="users" type="java.util.List" *#
#* @vtlvariable name="user" type="com.liferay.portal.model.User" *#
#* @vtlvariable name="themeDisplay" type="com.liferay.portal.theme.ThemeDisplay" *#
#* @vtlvariable name="languageUtil" type="com.liferay.portal.kernel.language.LanguageUtil" *#
#* @vtlvariable name="request" type="java.util.Map" *#
#* @vtlvariable name="httpUtil" type="com.liferay.portal.kernel.util.HttpUtil" *#
#* @vtlvariable name="htmlUtil" type="com.liferay.portal.kernel.util.HtmlUtil" *#
#* @vtlvariable name="obc" type="com.liferay.portal.util.comparator.UserLastNameComparator" *#
#* @vtlvariable name="serviceLocator" type="com.liferay.portal.velocity.ServiceLocator" *#
#* @vtlvariable name="teamLocalService" type="com.liferay.portal.service.TeamLocalServiceUtil" *#
#* @vtlvariable name="mbMessageLocalService" type="com.liferay.portlet.messageboards.service.MBMessageLocalServiceUtil" *#
#* @vtlvariable name="mbThreadLocalService" type="com.liferay.portlet.messageboards.service.MBThreadLocalServiceUtil" *#
#* @vtlvariable name="imageToken" type="com.liferay.portal.kernel.servlet.ImageServletToken" *#
#* @vtlvariable name="userLocalService" type="com.liferay.portal.service.UserLocalServiceUtil" *#
#* @vtlvariable name="groupIdCriteria" type="com.liferay.portal.kernel.dao.orm.Criterion" *#
#* @vtlvariable name="groupIdProp" type="com.liferay.portal.kernel.dao.orm.Property" *#
#* @vtlvariable name="threadMap" type="java.util.Map<java.lang.Long, java.lang.Integer>" *#
#* @vtlvariable name="q" type="com.liferay.portal.kernel.dao.orm.DynamicQuery" *#
#* @vtlvariable name="q2" type="com.liferay.portal.kernel.dao.orm.DynamicQuery" *#
#* @vtlvariable name="rfu" type="com.liferay.portal.kernel.dao.orm.RestrictionsFactoryUtil" *#
#* @vtlvariable name="pfu" type="com.liferay.portal.kernel.dao.orm.ProjectionFactoryUtil" *#
#* @vtlvariable name="ofu" type="com.liferay.portal.kernel.dao.orm.OrderFactoryUtil" *#
#* @vtlvariable name="msgs" type="java.util.List<com.liferay.portlet.messageboards.model.MBMessage>" *#

#set ($portletNamespace = $request.portlet-namespace)
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))

#if ($request.lifecycle == "RENDER_PHASE")

<body onload="${portletNamespace}getTable();">
<article>
  <h1 class="section-heading section-heading-b">
    <div>$title.data</div>
    <div class="section-heading-hr"></div>
  </h1>

  <div id='${portletNamespace}tablediv' style='width: 85%;'><!-- --></div>
</article>
</body>
<script type="text/javascript">

  var ${portletNamespace}table = new Object();

  var ${portletNamespace}ICON =
    '<img  class="icon" \
        src="http://my-liferay-site-cdn.com/osb-theme/images/spacer.png" \
        alt="Message Boards" title="Message Boards" \
        style="  background-image: url(\'/html/icons/_sprite.png\');\
            background-position: 50% -736px; \
        background-repeat: no-repeat; height: 16px; width: 16px;">';

  function ${portletNamespace}drawChart() {

    var html =
      '<div> \
         <table style="margin-bottom:0em;"> \
           <tbody>';

    for (i = 0; i < ${portletNamespace}table.length; i++) {
      html +=
        '<tr> \
          <td class="portlet-icon" style="padding-right:6px;"> \
          <table style="margin-top:-4px; margin-bottom:0px;">\
            <tr>\
            <td>\
              <span>' + ${portletNamespace}ICON + '</span> \
            </td>\
            </tr>\
            <tr>\
            <td>\
              <span style="color:#908E91; font-size:9px;">'+
                ${portletNamespace}table[i].msgCount +
              '</span>\
            </td>\
            </tr>\
          </table>\
          </td> \
          <td>\
          <div>\
            <h3 class="txt-n fs-11 m-0 o-h">\
            <span class="display-b m-tn3 m-b6">\
              <a href="/community/forums/-/message_boards/message/' +
                ${portletNamespace}table[i].msgid +'">'+
                ${portletNamespace}table[i].subject +
              '</a>\
            </span>\
            </h3>\
          </div>\
          </td>\
        </tr>';
    }
    html += '</tbody>\
      </table>\
    </div>';

    document.getElementById('${portletNamespace}tablediv').innerHTML = html;
  }

  function ${portletNamespace}getTable() {
    AUI().use(
      "aui-base", "aui-io-plugin", "aui-io-request",
      function(A) {
        A.io.request(
          '${request.resource-url}',
          {
            data: {
            },
            dataType: "json",
            on: {
              success: function(event, id, obj) {
                var responseData = this.get("responseData");
                  ${portletNamespace}table = responseData.jsonArray || [];

                  ${portletNamespace}drawChart();
              },
              failure: function(event, id, obj) {
              }
            }
          }
        );
      }
    );
  }

</script>
#elseif ($request.lifecycle == "RESOURCE_PHASE")
  #set ($logFactory = $portal.getClass().forName('com.liferay.portal.kernel.log.LogFactoryUtil'))
  #set ($log = $logFactory.getLog('mylog'))

  #set ($portalBeanLocator = $portal.getClass().forName("com.liferay.portal.kernel.bean.PortalBeanLocatorUtil"))
  #set ($jsonFactory = $portalBeanLocator.locate("com.liferay.portal.kernel.json.JSONFactoryUtil"))
  #set ($mbMessageLocalService = $portalBeanLocator.locate("com.liferay.portlet.messageboards.service.MBMessageLocalService.velocity"))
  #set ($mbThreadLocalService = $portalBeanLocator.locate("com.liferay.portlet.messageboards.service.MBThreadLocalService.velocity"))

  #set ($calClass = $portal.getClass().forName("java.util.GregorianCalendar"))
  #set ($mbMessageClass = $portal.getClass().forName("com.liferay.portlet.messageboards.model.MBMessage"))
  #set ($mbThreadClass = $portal.getClass().forName("com.liferay.portlet.messageboards.model.MBThread"))
  #set ($dqfu = $portal.getClass().forName("com.liferay.portal.kernel.dao.orm.DynamicQueryFactoryUtil"))
  #set ($pfu = $portal.getClass().forName("com.liferay.portal.kernel.dao.orm.ProjectionFactoryUtil"))
  #set ($ofu = $portal.getClass().forName("com.liferay.portal.kernel.dao.orm.OrderFactoryUtil"))

  #set ($now = $calClass.getInstance())
  #set ($weeksago = $calClass.getInstance())
  #set ($prevweeks = 0 - $getterUtil.getInteger($period.data))
  #set ($V = $weeksago.add(3, $prevweeks))


  #set ($q = $dqfu.forClass($mbThreadClass))
  #set ($rfu = $portal.getClass().forName("com.liferay.portal.kernel.dao.orm.RestrictionsFactoryUtil"))

  #set ($groupIdCriteria = $rfu.ne("categoryId", $getterUtil.getLong("-1")))
  #set ($V = $q.add($groupIdCriteria))

  #set ($groupIdCriteria = $rfu.eq("groupId", $getterUtil.getLong($scopeGroupId)))
  #set ($V = $q.add($groupIdCriteria))

  #set ($companyIdCriteria = $rfu.eq("companyId", $getterUtil.getLong($companyId)))
  #set ($V = $q.add($companyIdCriteria))

  #set ($statusCriteria = $rfu.eq("status", 0))
  #set ($V = $q.add($statusCriteria))

  #set ($lastPostDateCriteria = $rfu.between("lastPostDate", $weeksago.getTime(), $now.getTime()))
  #set ($V = $q.add($lastPostDateCriteria))

  #set ($V = $q.setProjection($pfu.property("threadId")))

  #set ($res1 = $mbMessageLocalService.dynamicQuery($q))
  #set ($q2 = $dqfu.forClass($mbMessageClass))

  #set ($inCriteria = $rfu.in("threadId", $res1))
  #set ($V = $q2.add($inCriteria))

  #set ($createDateCriteria = $rfu.between("createDate", $weeksago.getTime(), $now.getTime()))
  #set ($V = $q2.add($createDateCriteria))

  #set ($V = $q2.setProjection($pfu.projectionList().add($pfu.groupProperty("rootMessageId")).add($pfu.alias($pfu.rowCount(), "msgCount"))))
  #set ($V = $q2.addOrder($ofu.desc("msgCount")))
  #set ($V = $q2.setLimit(0, 7))

  #set ($res2 = $mbMessageLocalService.dynamicQuery($q2))

  #set ($jsonArray = $jsonFactory.createJSONArray())

  #foreach ($msgSum in $res2)

    #set ($rootMsgId = $msgSum.get(0))
    #set ($msgCount = $msgSum.get(1))
    #set ($subject = $mbMessageLocalService.getMessage($rootMsgId).getSubject())

    #set ($jsonObject = $jsonFactory.createJSONObject())
    #set ($V = $jsonObject.put("subject", $stringUtil.shorten($htmlUtil.escape($subject), 55)))
    #set ($V = $jsonObject.put("msgid", $rootMsgId))
    #set ($V = $jsonObject.put("msgCount", $msgCount))
    #set ($V = $jsonArray.put($jsonObject))
  #end
{
"jsonArray": $jsonArray
}
#end

 

Blogs
Excellent post james. Thanks for sharing. I always wanted to have some logging in velocity template. Thanks for the share.
Very useful, thanks. I am also thinking combining this with the information from this blog : http://www.liferay.com/web/jeffrey.handa/blog/-/blogs/11281793 (using java-code inside a hook accessible through Spring) It should be a good alternative in many usecasesemoticon
Thank James, this is great.

@ Armaz - I was thinking the exact same thing for logging from the script console. The following works when you set the script type to Groovy.

logFactory = String.getClass().forName('com.liferay.portal.kernel.log.LogFactoryUtil')
log = logFactory.getLog('mylog')
log.error('Logging from Groovy Script')
Awesome post James...Now things will be much easier to write LR apps..
A few more gotchas using preview mode (view_article_content) action:
- no javascript (AUI., Liferay.) works out of the box. It would be very usefull for rendering collapsible lists etc. What do I need to include to have it working?
- no $request variable, because the view_article_content action does not fill it. What can do here?
Is this a joke? You're trying to convince me that this is a "feature"?

The hoops you had to jump through above is astounding. In a JSP with the proper tags this all would have been reduced to ~10-15 lines. Instead, you're trying to use the wrong tools for the job.. you're developing crazy code in a textarea, it isn't versioned or deployable between different environments (e.g. QA to Prod), and it's in Velocity.

This is a bad approach all around. Stop using Velocity. Stop convincing people to paste their code into a text area. And make it really easy for me to query and transform content in my own custom portlets.

This is a huge example where Liferay is out of touch with developers and the marketplace.
thanks for this post.it is very useful for me .
Thank you James for the link to JIRA. I agree with the conclusions of the discussion there.
I just wanted a small workaround until the issue is resolved.
I was wrong about the $reserved-* variables. They are in place.
Remaining problems concerning the preview are the following:
- no usual JavaScripts included
- no $request variable.
If I try to preview an article using a template depending on these features, I get garbage.
I think, it is possible to solve these issues without doing a full "preview in a page" functionality. It is possible to include default JavaScript and fill the $request with all info excluding the page context if the page is not currently available.