Extending Liferay OSGi Modules

Recently I was working on a fragment bundle for a JSP override to the message boards and I wanted to wrap the changes so they could be disabled by a configuration property.

But the configuration is managed by a Java interface and set via the OSGi Configuration Admin service in a completely different module jar contained in an LPKG file in the osgi directory.

So I wondered if there was a way to weave in a change to the the Java interface to include my configuration item in a concept similar to the plugin extending a plugin technique.

And in fact there is and I'm going to share it with you here...

Creating The Module

So first we're going to be building a gradle module in a Liferay workspace, so you're going to need one of those.

In our modules directory we're going to create a new folder named message-boards-api-ext for containing our new module.  Actually the name of the folder doesn't matter too much so feel free to follow your own naming standards.

We need a gradle build file, and since we're in a Liferay workspace, our build.gradle file is pretty simple:

dependencies {
    compileOnly group: "biz.aQute.bnd", name: "biz.aQute.bndlib", version: "3.1.0"
    compileOnly group: "com.liferay", name: "com.liferay.portal.configuration.metatype", version: "2.0.0"
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0"
    compileOnly group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
    compileOnly group: "org.osgi", name: "org.osgi.core", version: "5.0.0"

    compile group: "com.liferay", name: "com.liferay.message.boards.api", version: "3.1.0"
}

jar.archiveName = 'com.liferay.message.boards.api.jar'

The dependencies mostly come from the build.gradle file from the module from the Liferay source found here: https://github.com/liferay/liferay-portal/blob/master/modules/apps/collaboration/message-boards/message-boards-api/build.gradle

We did add as a compile option the module that we're building a replacement for, in this case the com.liferay.message.boards.api module.

Also we are specifying the archive name that we are building that excludes the version number.  We're specifying the archive name so it matches the specifications from the Liferay override documentation: https://github.com/liferay/liferay-portal/blob/master/tools/osgi-marketplace-override-README.markdown

We also need a bnd.bnd file to build our module:

Bundle-Name: Liferay Message Boards API
Bundle-SymbolicName: com.liferay.message.boards.api
Bundle-Version: 3.1.0
Export-Package:\
  com.liferay.message.boards.configuration,\
  com.liferay.message.boards.display.context,\
  com.liferay.message.boards.util.comparator
Liferay-Releng-Module-Group-Description:
Liferay-Releng-Module-Group-Title: Message Boards

Include-Resource: @com.liferay.message.boards.api-3.1.0.jar

The bulk of the file is going to come directly from the original: https://github.com/liferay/liferay-portal/blob/master/modules/apps/collaboration/message-boards/message-boards-api/bnd.bnd.

The only addition to the file is the Include-Resource BND declaration.  As I previously covered in my blog post about OSGi Module Dependencies, this is the declaration used to create an Uber Module.  But for our purposes, this actually provides the binary source for the bulk of the content of our module.

By building an Uber Module from the source module, we are basically going to be building a jar file from the exploded original module jar, allowing us to have the baseline jar with all of the original content.

Finally we need our source override file, in this case we need the src/main/java/com/liferay/message/boards/configuration/MBConfiguration.java file:

package com.liferay.message.boards.configuration;

import aQute.bnd.annotation.metatype.Meta;

import com.liferay.portal.configuration.metatype.annotations.ExtendedObjectClassDefinition;

/**
 * @author Sergio González
 * @author dnebinger
 */
@ExtendedObjectClassDefinition(category = "collaboration")
@Meta.OCD(
	id = "com.liferay.message.boards.configuration.MBConfiguration",
	localization = "content/Language", name = "mb.configuration.name"
)
public interface MBConfiguration {

	/**
	 * Enter time in minutes on how often this job is run. If a user's ban is
	 * set to expire at 12:05 PM and the job runs at 2 PM, the expire will occur
	 * during the 2 PM run.
	 */
	@Meta.AD(deflt = "120", required = false)
	public int expireBanJobInterval();

	/**
	 * Flag that determines if the override should be applied.
	 */
	@Meta.AD(deflt = "false", required = false)
	public boolean applyOverride();
}

So the bulk of the code comes from the original: https://github.com/liferay/liferay-portal/blob/master/modules/apps/collaboration/message-boards/message-boards-api/src/main/java/com/liferay/message/boards/configuration/MBConfiguration.java

Our addition is the new flag value.

Now if we had other modifications for other classes, we would make sure we had the same paths, same packages, same class names, we would just have our changes in on top of the originals.

We could even introduce new packages and classes for our custom code.

Building The Module

Building is pretty easy, we just use the gradle wrapper to do the build:

$ ../../gradlew build

When we look inside of our built module jar, this is where we can see that our change did, in fact, get woven into the new module jar:

Exploded Module

We can see from the highlighted line from the compiled class that our jar definitely contains our method, so our build is good.  We can also see the original packages and resources that we get from the Uber Module approach, so our module is definitely complete.

Deploying The Module

Okay, so this is the ugly part of this whole thing, deployments are not easy.

Here's the restrictions that we have to keep in mind:

  1. The portal cannot be running when we do the deployment.
  2. The built jar file must be copied manually to the $LIFERAY_HOME/osgi/marketplace/override folder.
  3. The $LIFERAY_HOME/osgi/state folder must be deleted.
  4. If you have changed any web-type file (javascript, JSP, css, etc.) you should delete the relevant folder from the $LIFERAY_HOME/work folder.
  5. If Liferay deployes a newer version than the one declared in our bundle override, our changes may not be applied.
  6. Only one override bundle can work at a time; if someone else has an override bundle in this folder, your change will step on theirs and this may not be a option (Liferay may distribute updates or hot fixes as module overrides in this fashion).
  7. Support for $LIFERAY_HOME/osgi/marketplace/override was added in later LR7CE/DXP releases, so check that the version you are using has this support.
  8. You can break your portal if your module override does bad things or has bugs.

Wow, that is a lot of restrictions.

Basically we need to copy our jar manually to the $LIFERAY_HOME/osgi/marketplace/override folder, but we cannot do it if the application server is running.  The $LIFERAY_HOME/osgi/state folder should be whacked as we are changing the content of the folder.  And the bundle folder in $LIFERAY_HOME/work may need to be deleted so any cached resources are properly cleaned out.

For this change, we copy the com.liferay.message.boards.api.jar file to $LIFERAY_HOME/osig/marketplace/override folder and delete the state folder when the application server is down, then we can fire up the application server to see the outcome.

Conclusion

We can verify the change works by navigating to the Control Panel -> System Settings -> Collaboration -> Message Boards page and viewing the change:

Deployed Module Override

Be forewarned, however!  Listen when I emphasize the following:

Pay special attention to the restrictions on this technique!  This is not a "build, deploy and forget it" technique as each Liferay upgrade, fix pack or hot fix can easily invalidate your change, and every deployment requires special handling.
You can seriously break Liferay if you deliver bad code in your module override, so test the heck out of these overrides.
This should not be used in lieu of supported Liferay methods to extend or replace functionality (JSP fragment bundles, MVC command service overrides, etc.), this is just intended for edge cases where no other override/extension option is supported.

In other words:

 

Blogs
[...] So many folks have asked for it... "How do I change a JS file in a module?" "How do I change a CSS file in a module?" "How do i change a java file in a module?" Here's how to do those things:... [...] Read More
Hi David,

Thanks for this informative blog!!!
Actually we are migrating our portal from Liferay 6.2 to 7. Everything has been upgraded except session time out feature which we have customized in Liferay 6.2. In 6.2 we have customized session.js & notice.js files for achieving our goal. But in Liferay 7 it seems that customization of such js is not a straight forward thing. I followed your blog & tried to extend com.liferay.frontend.js.web OOB osgi bundle. For testing purpose I did a minor text change inside var SessionDisplay = A.Component.create() method of session.js file. code snippet is below.

instance._warningText = Liferay.Language.get('due-to-inactivity-your-session-will-expire') + '-custom';
instance._warningText = Lang.sub(
instance._warningText,
[
'<span class="countdown-timer">{0}</span>',
host.get('sessionLength') / 60000,
'<a class="alert-link" href="#">' + Liferay.Language.get('extend') + '</a>'
]
);

after this my "-custom" text is appearing with the message but timer/extend functionality is breaking. I am getting below error in console

00:27:23,285 WARN [http-nio-8080-exec-4][code_jsp:181] {code="500", msg="", uri=/o/frontend-js-web/liferay/available_languages.jsp}
javax.servlet.ServletException: Servlet execution threw an exception
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:315)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)

This file(available_languages.jsp) is also throwing 500 error in browser
http://localhost:8080/o/frontend-js-web/liferay/available_languages.jsp?browserId=other&themeId=mytolltheme_WAR_mytolltheme&colorSchemeId=01&minifierType=js&languageId=en_US&b=7010&t=1493338110858

I checked the final jar which I am keeping inside osgi\marketplace\override & found that it already contains available_languages.jsp inside. Can you please help me out on this.
Thanks in advance.


Regards,
Lalit
Simple and useful .
After following your , the changes were not reflected on the page.
So I upgraded to patch liferay-fix-pack-de-12-7010.zip to see the changes

thanks
[...] Nisha Rani: I need to add and update a method in AssetCategoryAdminPortlet.java class. Is it possible to do? No. if yes, please guide me how to do it in Liferay DXP. Well, there is the "module... [...] Read More
My project has dependencies on Liferay libraries, but all that seems available are CE libraries, which aren't the jars we get in our fixpacks. Is there a way to get the EE version of these libraries?
As a DXP customer you have access to the download area where you should be able to get the development artifacts.
I've got the ability to download the release bundled with Tomcat, the source, OSGI Dependencies, and Dependencies, but none of these seems to have the full set of jar files I'm looking for. Should I be going through the lkpgs?
[...] From a new Liferay developer perspective, the main roadblack you might encounter with them is when you want to consume API exposed by one of those private modules, or if you want to extend one of... [...] Read More
@David: Thanks for wonderful bug. I have extended "portal-workflow-task-web" module and its works fine. Now, I need to modify some services "portal-workflow-kaleo-service" module. So can we extend service module?

Any help will be really appreciated.
Really sorry for typo.

@David: Thanks for wonderful blog. I have extended "portal-workflow-task-web" module and its works fine. Now, I need to modify some methods of "portal-workflow-kaleo-service" module. So can we extend service module?

Any help will be really appreciated.

Dear David, thanks a lot for your post, it was very helpful!I followed your instructions to overwrite layout-admin-web (the class LayoutAssetRendererFactory), more specifically, to add setLinkable(true); to its constructor.However, when building I get errors like:error  : Missing super class for DS annotations: com.liferay.application.list.BasePanelApp from com.liferay.layout.admin.web.internal.application.list.GroupPagesPanelAppAny help would be appreciated, thanks!