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'))
$log.error($msgCount)
or
$log.error("Hi There").
Creating references for arbitrary JVM classes
#set ($calClass = $portal.getClass().forName("java.util.GregorianCalendar"))
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))
period
structure element.
-
The first one (
$q
) queries forMBThread
entities that have acategoryId
of -1 (MBThreads that do not have acategoryId
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 thelastPostDate
must be between my desired time period start and end. Finally, I am not interested in all of theMBThread
entity - I just need thethreadId
. So my query includes a Projection that only returns thethreadId
. -
The second query (
$q2
) queries for allMBMessage
entities that match my new critieria: they must have athreadId
of one of the threads identified in the first query (hence thein
criteria), and the message'screateDate
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))
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:
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
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 ofvar
. 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))
$scopeGroupId
a Long), whereas
#set ($scopeGroupId = $request.theme-display.scope-group-id)
-
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
#* @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
source
ReplyDeleteGreat Article I love to read your articles because your writing style is too good, its is very very helpful for all of us and I never get bored while reading your article because it becomes more and more interesting from the starting lines until the end. So Thank you for sharing a COOL Meaningful stuff with us Keep it up..!
Datawarehousing Training in Chennai
Thanks for the post, I am techno savvy. I believe you hit the nail right on the head. I am highly impressed with your blog.
ReplyDeleteIt is very nicely explained. Your article adds best knowledge to our Java Online Training from India.
or learn thru Java Online Training from India Students.
Some us know all relating to the compelling medium you present powerful steps on this blog and therefore strongly encourage
ReplyDeletecontribution from other ones on this subject while our own child is truly discovering a great deal.
Have fun with the remaining portion of the year.
Selenium training in bangalore
Selenium training in Chennai
Selenium training in Bangalore
Selenium training in Pune
Selenium Online training
buy heroin online
ReplyDeletebuy alprazolam powder online
buy cocaine online
buy mephedrone online
buy 3meo pcp online
buy xenax online
buy pills online
buy fentanyl powder online
buy scopolamine powder online
buy vyvanse powder online
buy coke powder online
buy research powder online
https://k2incenseonlineheadshop.com/
ReplyDeleteinfo@k2incenseonlineheadshop.com
k2incenseonlineheadshop Buy liquid incense cheap Buy liquid incense cheap For Sale At The Best Incense Online Shop
chemicalmedshop.com Buy liquid incense cheap ,Incense for sale online offers ,Liquid Spice ,k2 chemical spray for sale , where to buy k2 near me ,K2 E-LIQUID. liquid k2 , k2 spice spray ,herbal incense for sale , Liquid Herbal Incense, cheap herbal incense , strong herbal incense for sale , Liquid k2 on paper , Liquid K2 , Legal High Incense , liquid herbal incense for sale, Order Strong Liquid Incense , Herbal Incense For Sale , Buy Vape Cartridges , Cheap K2 Spice , Legal Potpourri , Buy Herbal Incense Discrete , Legit Herbal Incense Website , Liquid Incense Overnight Delivery , Buy Potpourri With Credit Cards , buy herbal incense with debit card, , Where To Order Liquid Incense Online , Buy Liquid Incense With Bitcoin , Buy K2 Liquid Incense On Paper In USA , Strongest Incense In USA
ReplyDeleteThank you for very useful information.
ReplyDeleteLuxury Series Stainless Steel Water Tanks in India