Liferay Module JSP Override

Liferay Module JSP Override

Module JSP Override Using OSGi Fragment


Overview


Liferay provides a lot of out-of-the-box applications, which can be used for building portals: login widget, blogs and forums, wiki or web contents. We often need to customize a particular widget UI according to business needs. In such cases we need to modify JSP files for that widget’s module. Module JSP Override is a solution here. There are several ways to proceed with Module JSP Override, and OSGi Fragment is one of them. In this article we’ll review what are Module JSPs and OSGi Fragments, how to find the target JSP file and how to implement a custom OSGi Fragment for Module JSP Override.


What is a Module JSP?


Liferay 6.2 (and previous versions) had a monolith architecture. Liferay was deployed as a single application under the ROOT context, and all the JSP files were stored in webapps/ROOT/html folder. For such JSPs customization a JSP hook approach was used.

In Liferay 7 the modularity was introduced: liferay core was splitted up into multiple OSGi modules. Each module is deployed now as a part of liferay package (*.lpkg file) in the osgi/marketplace directory:

There are API (“* - API.lpkg”) and Implementation (“* - Impl.lpkg”) liferay packages. Each of them contains a set of OSGi modules (JAR files). For example, for Blogs we have:

And each of the modules contains module-specific classes and/or JSP files. Usually, JSP files are stored in the “*.web-*.jar” modules (inside /META-INF/resources directory). For example for, com.liferay.blogs.web-6.0.89.jar has the following structure:

These JSP files within a specific module are considered as Module JSPs, and can be customized with the Module JSP Override.

Note: for Core JSP Override check this blog.


What is an OSGi Fragment?


An OSGi Fragment (Fragment Bundle) is a bundle that can be attached to a host bundle. Definitions of the fragment bundle are appended to the host bundle before it’s resolved. Therefore, a fragment bundle is treated as part of the host bundle. 

Note: check more info in the OSGi Specification:

Using the OSGi Fragment approach we can create a custom bundle that provides the customized JSP files, which are appended to the host bundle. During Fragment Bundle deployment the host bundle is stopped, “merged” with the fragment one, and then started again (becomes “Active”) with the provided customization. Fragment bundle remains “Resolved”, it’s a final state for such bundles. 

In the Liferay sources an example of OSGi Fragment module is adaptive-media-blogs-web-fragment, which overrides the full_content.jsp JSP file from blogs-web module by a custom one (full_content.jsp) and introduces additional HTML content transformation. If we check the bundle descriptor for this module - we’ll see that it contains the following line:


Fragment-Host: com.liferay.blogs.web;bundle-version="[1.1.0,7.0.0)"


Fragment-Host header here links the OSGi Fragment bundle to the host bundle using the host bundle symbolic name and version. Host bundle symbolic name is a Bundle-SymbolicName header from the host bundle descriptor (in this sample - bnd.bnd). Host bundle version can be specified in a different way:


Specific version, sample: bundle-version="6.0.89"

This way we specify the highest acceptable version of the host bundle. If we upgrade Liferay to the next version (and the host bundle version increases), the OSGi fragment bundle will no longer work. 

Note: it’s better to rely on the bundle version displayed in Gogo Shell than the one in sources, because they might not match (and fragment bundle won’t work if we specify the higher version):

Range of versions, sample: bundle-version="[6.0.0,7.0.0)"

This way we specify a range of acceptable versions for the host bundle. In this sample versions from 6.0.0 (inclusive) to 7.0.0 (exclusive) are acceptable. This means that even after portal upgrades the OSGi fragment will still work, unless major updates happen to the host module (6.x -> 7.0).


Keep in mind: OSGi Fragments can be used not only for JSP overrides, but also for other types of customization - exporting internal classes from host bundle, modifying configuration, overriding CSS or JS files, etc. See a nice article on this topic.


How to create a custom OSGi Fragment Bundle for Module JSP Override?


To create a custom OSGi Fragment Bundle we need to perform the following steps:

  1. Find the required host bundle.

  2. Find the target JSP(s) to override.

  3. Create a module for OSGi Fragment.

  4. Copy JSPs to override and make required changes

  5. Deploy the module and check results.


In this article we’ll customize a Login Widget to display a custom title on the login page, and a custom hint message on the forgot password page using the described steps.


1. Find the required host bundle


We can find the required module from the Gogo Shell using the lb <name> command (in this sample we list bundles with “login” pattern):

In the Liferay sources all the modules are located inside the modules/apps directory. For the login widget it’s the login-web module. 

Note: almost all modules with portlets are named with xxx-web pattern, so it should be easy to find them. If it’s still not clear what is the target module - a global search through Liferay sources can help.


2. Find the target JSP(s) to override.


Once a module is found - we can search for the JSPs that need to be customized. In our case we need to override two of them - for the landing page of the login widget, and for the “forgot password” functionality.

JSPs in the OSGi modules are stored inside the src/main/resources/META-INF/resources directory, in our case - resources. Sometimes (like in this sample) it’s quite obvious what the required JSPs are. But it’s often not that easy, and we need to explore the portlet’s code.

If we click the Forgot Password link in the Sign In widget - we’ll see the following URL:


http://{portal-url}?p_p_id=com_liferay_login_web_portlet_LoginPortlet&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&_com_liferay_login_web_portlet_LoginPortlet_mvcRenderCommandName=/login/forgot_password&saveLastPath=false


Important things here are the portlet name, and the Render MVC Command name. If we explore the portlet’s definition - we’ll see that the default view template is a /login.jsp:

Once we find the MVC Resource Command by it’s name (/login/forgot_password) - we’ll see the JSP file it returns:

With that, required JSPs for customization in our case are login.jsp and forgot_password.jsp


3. Create a module for OSGi Fragment.


Now, when we know which JSPs we need to customize - we can create a module for OSGi Fragment Bundle in our Liferay Workspace:


build.gradle is a regular one:


dependencies {

  compileOnly group: "com.liferay.portal", name: "release.portal.api"

}


In the bnd.bnd file we need to specify the Fragment-Host header:


Bundle-Name: LifeDev Module JSP Override

Bundle-SymbolicName: com.lifedev.module.jsp.override

Bundle-Version: 1.0.0

Fragment-Host: com.liferay.login.web;bundle-version="[6.0.0,7.0.0)"


It should link to the Bundle-SymbolicName of the host bundle. Host bundle version can be specified either precisely, or as a range (see more information "What is an OSGi Fragment?" section). We’ll leave it as a range to make the fragment module work after potential portal upgrades.


4. Copy JSPs to override and make required changes


Now we need to copy JSP files that needs to be customized from portal sources:

into the src/main/resources/META-INF/resources directory in our module.

For the login.jsp file we can add a custom title at the top of login container, if user is not signed in:

For the forgot_password.jsp we can add a hint message on the top of the JSP file in the same way:

But, as long as we need to make customization at the very top of the JSP file - we may include the original file instead of copy-pasting it:

The original JSP can be referenced with the portal prefix (e.g. /forgot_password.portal.jsp) and be included using in the following way:


<liferay-util:include

page="/forgot_password.portal.jsp" servletContext="<%= application %>" />


5. Deploy the module and check results.


After deploying the OSGi Fragment Bundle we’ll see in logs, that the host bundle is stopped and started again (with customization provided by our fragment bundle):  


[BundleStartStopLogger:80] STOPPED com.liferay.login.web_6.0.37 [792]

[BundleStartStopLogger:77] STARTED com.liferay.login.web_6.0.37 [792]


 Our bundle should remain in the Resolved state (which is normal for fragment bundles):

Once we navigate to the login page - a custom title should be displayed:

On the forgot password page there should be a custom hint message:


Enjoy 😏

Comments

  1. Hi Author,
    Thanks for this post, it is really helpful.
    I have followed the same steps for Liferay Login Web module. But after the deployment I have noticed the custom module-fragment is in Installed status instead of Resolved. The status would be changed to Resolved from Installed if I add the dependency compileOnly group: 'com.liferay', name: 'com.liferay.login.web', as mentioned below
    dependencies {
    compileOnly group: "com.liferay.portal", name: "release.portal.api"
    compileOnly group: 'com.liferay', name: 'com.liferay.login.web'
    }
    My bnd.bnd is
    Bundle-Name: practice-login-module-fragment
    Bundle-SymbolicName: practice-login-module-fragment
    Bundle-Version: 1.0.0
    Fragment-Host: com.liferay.login.web;bundle-version="[6.0.0,7.0.0)"
    Portal-Bundle-Version: 7.4.3.90

    As I know, com.liferay.login.web, this entry is not required in build.gradle for module fragment, but without this my module-fragment is not working and it is in Installed status.
    Could you please tell me what changes are required to resolve this?

    ReplyDelete

Post a Comment

Popular posts from this blog

Liferay Search Container Example

Liferay DXP - max upload file size

Liferay Keycloak integration