Dynamic Widget

Genesis

My colleague Vagif proposed a new way to develop a Liferay dynamic widget (the best name I can think of for now) with these pieces:

  1. Use the “Basic Web Content” structure or define a new structure.
  2. For each dynamic widget type, define a new display template.
  3. Create an OSGi service that returns a data model to be used by the display template.
  4. Create a web content article with the above structure/template pair.
  5. Place this web content article onto the page with Web Content Display (OOTB).

All business logics are in the OSGi service. The structure can serve as a preference or parameter holder for the display template. The display template draws most meaningful data from the model returned by the service.

You can build a portlet-less Liferay site with this "Crafter-ish" approach.

Rationales

A typical Liferay feature has program code spread out among many technologies: service Java code, portlet Java code, template code, template Java code (JSP), language files, property files, XML files, etc. You often find a feature morphs into some JSP files with 100+ lines of Java code, a 2000 line portlet, plus several service builders, all competing for business logic implementations and MVC roles.

A Simpler Pattern

Dynamic widget may serve as a Liferay implementation pattern that simplifies and promotes good programming practices (I think this is better pattern, but some may disagree):

  • Write Java code in and only in service modules (no portlet).
  • Implement all business logic in services.
  • The display template calls a single service to retrieve a data model.
  • The display template then renders the model with almost no business logic.

A Simple Rule:

  • Let the services build the most foolproof model for the template to render it in the most simplistic way.

There is nothing stopping a template from calling multiple services then juggle multiple models to render the view. However, do your self a favor, write a new service method that combines all of them into a single model for the template. You will thank yourself later.

Why Freemarker?

Cannot use JSP may be a down side of dynamic widget, or is it?

Compared to JSP, template languages like Freemarker cannot mix Java code with UI code, so they promote a cleaner separation of view from model and controller. Some may argue that template code getting the model from services is controller-ish. Agree, but beyond that keeping Freemarker code simple is not hard at all, because complex Freemarker code is ugly and painful to write, not to mention tough to debug, log, handle errors, etc.

Pseudo Web Content

Dynamic widget is a Liferay web content, but its "content" is not in its web content structure. The content comes from the services. If you think of a Web Content Display rendering a dynamic widget in a page as calling a function in a program, then its web content structure is like the parameter for that function call.

The widget's web content may be empty, which is similar to calling a function with no parameter. The service that builds the model for the template has everything it needs. In other instances, the structure can have variables used as parameters for calling the service or be used directly by the template just like any other web content.

Search and Preview

Note that variables in the web content structure can be searchable, making them available to Liferay search and Assert Publisher. For example, a dynamic widget may be titled "Annual Sales Figures 2018", which renders a bar chart from a model provided by a service module. Then this widget may be a search hit, along with its pretty chart if you wish. You can't have that if the chart is implemented in a portlet.

Don't forget, you can easily mark the web content not searchable too:

Another convenience of dynamic widget over portlet is preview. Open Liferay Control Panel > (a site) > Web Content. You can preview a widget independent of a page from its Options menu:

Managing Display Templates

For the rest of this topic, lets call the template associated with the web content structure the parent template. The parent template can render the entire widget all by itself, but it's much more likely for the parent template to use other templates with the '<#include />' Freemarker directive.

Here we discuss three ways to develop templates for dynamic widgets.

Liferay Generic Templates

Open Liferay Control Panel > (a site) > Web Content > (corner Options menu) > Templates. Note the first column of table view called "ID". The value of that column is the template ID, also known as template key.

From this Templates view, you can create a template and leave the "Structure" field empty. That creates a generic template. Then in a parent template, you can use a generic template like this:

<#include "${templatesPath}/TEMPLATE_KEY"/>

With this framework, you can implement everything inside Liferay user interface. However, exporting then importing generic templates a LAR file will invalidate all template keys. All parent templates must be manually modified with new keys.

Package Templates In A Module

In the attached xyz-templates.zip, "modules/my-templates" demonstrates packaging Freemarker templates in a OSGi module. The key for parent templates to use templates in this module is the "Web-ContextPath" header:

modules/my-templates/bnd.bnd
Bundle-Name: My Templates Bundle
Bundle-SymbolicName: my.liferay.templates
Bundle-Version: 1.0.0
Web-ContextPath: /my-templates

In a parent template, use the Liferay infix _SERVLET_CONTEXT_ like this:

<#include "my-templates_SERVLET_CONTEXT_/widget/software-project.ftl" />

where before the infix is the "Web-ContextPath" value, and after which is the template path in the module project under "src/main/resources".

The Liferay Freemarker engine allows a template to include sibling templates in the same module using relative path of the current template. For example,

modules/my-templates/src/main/resources/widget/software-project.ftl
<h3>software-project.ftl</h3>
From parent path:
<#include "../common/left-navigation.ftl"/>
From child path:
<#include "more/right-navigation.ftl"/>

Now the fun part of how Freemarker templates find and call services. First of all, you need to remove Liferay's default restriction on template variables by creating this configuration file as the following:

LIFERAY_HOME/osgi/configs/com.liferay.portal.template.freemarker.configuration.FreeMarkerEngineConfiguration.cfg
restrictedVariables=

Then a template can access services in several ways, for example, using the "staticUtil" or "serviceLocator" variables:

modules/my-templates/src/main/resources/widget/software-project.ftl
Using restricted variables:
<#assign userLocalServiceUtil = staticUtil['com.liferay.portal.kernel.service.UserLocalServiceUtil'] />
${userLocalServiceUtil.getDefaultUserId(companyId)},
<#assign userLocalService = serviceLocator.findService('com.liferay.portal.kernel.service.UserLocalService') />
${userLocalService.getDefaultUser(companyId).originalEmailAddress}

Refer to the "Extras" section for a complete list of all Liferay variables available to Freemarker templates.

Package Templates In A Theme

The attached xyz-templates.zip also includes an example theme in "wars/my-theme". Following the folder convention of a theme, templates in the projects are under folder "src/main/webapp/templates". The Gradle build automatically generates a "Web-ContactPath" header with the value of the project folder. Therefore, a parent template can reference a template in this theme as:

<#include "my-theme_SERVLET_CONTEXT_/templates/widget/software-project.ftl" />

Other aspects of templates in a theme are identical to ones in a module.

Deployment

You can deploy both the module JAR and the theme WAR files by copying them to the Liferay auto deploy folder. Here is what they look like when running:

$ telnet localhost 11311
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
____________________________
Welcome to Apache Felix Gogo
 
g! lb my
START LEVEL 20
   ID|State      |Level|Name
  759|Active     |   10|Liferay My Account Web (1.0.11)
  826|Active     |   10|Liferay Portal Security AntiSamy (2.0.12)
  994|Active     |   10|Liferay Site My Sites Web (1.0.8)
 1232|Active     |   10|My Templates Bundle (1.0.0)
 1237|Active     |   10|my-theme (7.0.10)
g!

Extras

A complete list of all Liferay variables available to Freemarker templates, including their implementation class names and snippets of Liferay source code:

FreeMarkerEngineConfiguration.java:
    @Meta.AD(
        deflt = "serviceLocator|utilLocator|objectUtil|staticFieldGetter|staticUtil",
        required = false
    )
    public String[] restrictedVariables();
 
com.liferay.portal.template.TemplateContextHelper.getHelperUtilities(, false)
{
accountPermission=com.liferay.portal.service.permission.AccountPermissionImpl,
arrayUtil=com.liferay.portal.kernel.util.ArrayUtil_IW,
auditMessageFactoryUtil=com.liferay.portal.security.audit.internal.AuditMessageFactoryImpl,
auditRouterUtil=null,
browserSniffer=com.liferay.portal.servlet.BrowserSnifferImpl,
calendarFactory=com.liferay.portal.util.CalendarFactoryImpl,
commonPermission=com.liferay.portal.service.permission.CommonPermissionImpl,
dateFormatFactory=com.liferay.portal.util.FastDateFormatFactoryImpl,
dateFormats=com.liferay.portal.util.FastDateFormatFactoryImpl,
dateTool=May 5, 2018 10:34:58 AM,
dateUtil=com.liferay.portal.kernel.util.DateUtil_IW,
escapeTool=org.apache.velocity.tools.generic.EscapeTool,
expandoColumnLocalService=com.liferay.portlet.expando.service.impl.ExpandoColumnLocalServiceImpl,
expandoRowLocalService=com.liferay.portlet.expando.service.impl.ExpandoRowLocalServiceImpl,
expandoTableLocalService=com.liferay.portlet.expando.service.impl.ExpandoTableLocalServiceImpl,
expandoValueLocalService=com.liferay.portlet.expando.service.impl.ExpandoValueLocalServiceImpl,
getterUtil=com.liferay.portal.kernel.util.GetterUtil_IW,
groupPermission=com.liferay.portal.service.permission.GroupPermissionImpl,
htmlUtil=com.liferay.portal.util.HtmlImpl,
httpUtil=com.liferay.portal.util.HttpImpl,
imageToken=com.liferay.portal.webserver.WebServerServletTokenImpl,
imageToolUtil=com.liferay.portal.image.ImageToolImpl,
iteratorTool=org.apache.velocity.tools.generic.IteratorTool,
jsonFactoryUtil=com.liferay.portal.json.JSONFactoryImpl,
languageUtil=com.liferay.portal.language.LanguageImpl,
layoutPermission=com.liferay.portal.service.permission.LayoutPermissionImpl,
listTool=org.apache.velocity.tools.generic.ListTool,
localeUtil=com.liferay.portal.kernel.util.LocaleUtil,
locationPermission=com.liferay.portal.service.permission.OrganizationPermissionImpl,
mathTool=org.apache.velocity.tools.generic.MathTool,
numberTool=org.apache.velocity.tools.generic.NumberTool,
organizationPermission=com.liferay.portal.service.permission.OrganizationPermissionImpl,
paramUtil=com.liferay.portal.kernel.util.ParamUtil_IW,
passwordPolicyPermission=com.liferay.portal.service.permission.PasswordPolicyPermissionImpl,
portal=com.liferay.portal.util.PortalImpl,
portalPermission=com.liferay.portal.service.permission.PortalPermissionImpl,
portalUtil=com.liferay.portal.util.PortalImpl,
portletModeFactory=com.liferay.portal.kernel.portlet.PortletModeFactory_IW,
portletPermission=com.liferay.portal.service.permission.PortletPermissionImpl,
portletProviderAction={ADD=ADD, BROWSE=BROWSE, MANAGE=MANAGE, EDIT=EDIT, PREVIEW=PREVIEW, VIEW=VIEW},
portletURLFactory=com.liferay.portlet.PortletURLFactoryImpl,
prefsPropsUtil=com.liferay.portal.util.PrefsPropsImpl,
propsUtil=com.liferay.portal.util.PropsImpl,
randomizer=com.liferay.portal.kernel.util.Randomizer,
rolePermission=com.liferay.portal.service.permission.RolePermissionImpl,
saxReaderUtil=com.liferay.portal.xml.SAXReaderImpl,
serviceLocator=com.liferay.portal.template.ServiceLocator,
sessionClicks=com.liferay.portal.kernel.util.SessionClicks_IW,
sortTool=org.apache.velocity.tools.generic.SortTool,
staticFieldGetter=com.liferay.portal.kernel.util.StaticFieldGetter,
stringUtil=com.liferay.portal.kernel.util.StringUtil_IW,
timeZoneUtil=com.liferay.portal.kernel.util.TimeZoneUtil_IW,
unicodeFormatter=com.liferay.portal.kernel.util.UnicodeFormatter_IW,
unicodeLanguageUtil=com.liferay.portal.language.UnicodeLanguageImpl,
userGroupPermission=com.liferay.portal.service.permission.UserGroupPermissionImpl,
userPermission=com.liferay.portal.service.permission.UserPermissionImpl,
utilLocator=com.liferay.portal.template.UtilLocator,
validator=com.liferay.portal.kernel.util.Validator_IW,
velocityPortletPreferences=,
webServerToken=com.liferay.portal.webserver.WebServerServletTokenImpl,
windowStateFactory=com.liferay.portal.kernel.portlet.WindowStateFactory_IW,
}

.