You can completely override JSPs using OSGi fragments. This approach is powerful but can make things unstable when the host module is upgraded:
- By overriding an entire JSP, you might not account for new content or new widgets essential to new host module versions.
- Fragments are tied to a specific host module version. If the host module is upgraded, the fragment detaches from it. In this scenario, the original JSPs are still available and the module is functional (but lacks your JSP enhancements).
- Liferay cannot guarantee that JSPs overridden by fragments can be upgraded.
Using OSGi fragments to override JSPs is a bad practice, equivalent to using Ext plugins to customize Liferay DXP. They should only be used as a last resort. Liferay’s API based approaches to overriding JSPs (i.e., Dynamic Includes and Portlet Filters), on the other hand, provide more stability as they customize specific parts of JSPs that are safe to override. Also, the API based approaches don’t limit your override to a specific host module version. If you are maintaining existing JSP overrides that use OSGi fragments, however, this tutorial explains how they work.
An OSGi fragment that overrides a JSP requires these two things:
The host module’s symbolic name and version in the OSGi header
The original JSP with any modifications you need to make.
For more information about fragment modules, you can refer to section 3.14 of the OSGi Alliance’s core specification document.
There are two players in this game: the fragment and the host. The fragment is a parasitic module that attaches itself to a host. That sounds harsh, so let’s compare the fragment-host relationship to the relationship between a pilot fish and a huge, scary shark. It’s symbiotic, really. Your fragment module benefits by not doing much work (like the pilot fish who benefits from the shark’s hunting prowess). In return, the host module gets whatever benefits you’ve conjured up in your fragment’s JSPs (for the shark, it gets free dental cleanings!). To the OSGi runtime, your fragment is part of the host module.
Your fragment must declare two things to the OSGi runtime regarding the host module:
The Bundle Symbolic Name of the host module. This is the module containing the original JSP.
The exact version of the host module to which the fragment belongs.
Both are declared using the OSGi manifest header
Supplying a specific host module version is important. If that version of the module isn’t present, your fragment won’t attach itself to a host, and that’s a good thing. A new version of the host module might have changed its JSPs, so if your now-incompatible version of the JSP is applied to the host module, you’ll break the functionality of the host. It’s better to detach your fragment and leave it lonely in the OSGi runtime than it is to break the functionality of an entire application.
There are two possible naming conventions for targeting the host original JSP:
original. For example, if the original JSP is in the folder
/META-INF/resources/login.jsp, then the fragment bundle should contain a JSP
with the same path, using the following pattern:
<liferay-util:include page="/login.original.jsp" (or login.portal.jsp) servletContext="<%= application %>" />
After that, make your modifications. Just make sure you mimic the host module’s
folder structure when overriding its JAR. If you’re overriding Liferay’s login
login.jsp for example, you’d put your own
If you must post-process the output, you can update the pattern to include
Liferay DXP’s buffering mechanism. Below is an example that overrides the original
<%@ include file="/init.jsp" %> <liferay-util:buffer var="html"> <liferay-util:include page="/create_account.portal.jsp" servletContext="<%= application %>"/> </liferay-util:buffer> <liferay-util:buffer var="openIdFieldHtml"><aui:input name="openId" type="hidden" value="<%= ParamUtil.getString(request, "openId") %>" /> </liferay-util:buffer> <liferay-util:buffer var="userNameFieldsHtml"><liferay-ui:user-name-fields /> </liferay-util:buffer> <liferay-util:buffer var="errorMessageHtml"> <liferay-ui:error exception="<%= com.liferay.portal.kernel.exception.NoSuchOrganizationException.class %>" message="no-such-registration-code" /> </liferay-util:buffer> <liferay-util:buffer var="registrationCodeFieldHtml"> <aui:input name="registrationCode" type="text" value=""> <aui:validator name="required" /> </aui:input> </liferay-util:buffer> <% html = com.liferay.portal.kernel.util.StringUtil.replace(html, openIdFieldHtml, openIdFieldHtml + errorMessageHtml); html = com.liferay.portal.kernel.util.StringUtil.replace(html, userNameFieldsHtml, userNameFieldsHtml + registrationCodeFieldHtml); %> <%=html %>
To use an internal (unexported) host package, the fragment must explicitly
exclude the package from its
Import-Package: manifest header. For example,
Import-Package header excludes packages that match
Unless you explicitly exclude the package, bnd adds the package to the
Import-Package: header. Attempting to start the fragment while requiring an
unexported package fails because the package is an unresolved requirement. For
this reason, make sure to exclude such packages from your fragment’s
Each fragment has full access to the host packages, including its internal (unexported) packages already.
Now you can easily modify the JSPs of any application in Liferay.