In this post I would like to explain what System Checkers in Liferay are, how you can implement one and what you can achieve using it. As an example I will use a real life scenario - what if you have a fragment which depends on another OSGi module and this module stops exporting the package you need in your fragment. In such scenario Liferay will sadly not report any error even though your fragment basically stops working. Lets find out how we can use system checkers to find such situations or how you can use them for any other checks you need.
Some introduction
So the situation I described at the beginning is the real issue I've experienced. In a project I work on we have quite a lot of different fragments. Some of these fragments make some huge changes in the basic views while others do only a tiny changes like adding a field, button or some text. Sometimes these fragments use constants from another API modules.
In my blog samples project I created such fragment as an example. This fragment is meant to add a simple text in the "Password" page of editing user view (Under Control Panel -> Users and Organizations -> Edit one of the users).
This fragment uses the API module for a constant text (lets skip translations in this example). The added code looks like this:
<p>
<%=FakeConstants.FAKE_CONSTANT%>
</p>
Nothing really fancy here if you are familiar with fragments modules.
Anyway the issue with this fragment is that the FakeConstants class package is not exported by the API module's bnd.bnd file. Because of this the fragment will not start. It will stay in the Installed OSGi state
But hey - developer would notice that the fragment has not started
Of course. He would. And even if he pushed that without testing someone else would. For example tester or project manager or even a client. That's true.
But what if the module has been created few months ago, everything was working but then someone moved our class to another package because he thought it's only used internally in the same module? Or even removed the Export-Package line from bnd.bnd?
Well in such scenario the edit password page, which has worked for long time, might not be retested and the issue could've been overseen. Of course the page would still work but without changes from our fragment module. It would just use the default Liferay's view. This can be fine in some cases but maybe in some cases it's a legal requirement which your fragments fulfills for example adds some specific warning text required in your country. Or maybe your fragment adds some extra fields to the form and another module you use needs these fields otherwise the form won't work.
Doesn't Liferay give you some error on startup if the bundle couldn't be resolved?
Well actually it does but only for normal modules. In order to show this I created another module: not-resolved-bundles-web which is a basic portlet and it also uses the same, not exported API. In this case on startup there are errors in the logs:
2022-07-22 07:32:59.859 ERROR [fileinstall-directory-watcher][DirectoryWatcher:1173] Unable to start bundle: file:/opt/liferay/osgi/modules/com.pydyniak.blog.samples.not.resolved.bundles.web.jar
2022-07-22T07:32:59.860166363Z com.liferay.portal.kernel.log.LogSanitizerException: org.osgi.framework.BundleException: Could not resolve module: com.pydyniak.blog.samples.not.resolved.bundles.web [1503]_ Unresolved requirement: Import-Package: com.pydyniak.blog.samples.not.resolved.bundles.api_ [Sanitized]
2022-07-22T07:32:59.860172750Z at org.eclipse.osgi.container.Module.start(Module.java:444) ~[org.eclipse.osgi.jar:?]
2022-07-22T07:32:59.860182947Z at org.eclipse.osgi.internal.framework.EquinoxBundle.start(EquinoxBundle.java:428) ~[org.eclipse.osgi.jar:?]
2022-07-22T07:32:59.860184568Z at com.liferay.portal.file.install.internal.DirectoryWatcher._startBundle(DirectoryWatcher.java:1156) [bundleFile:?]
2022-07-22T07:32:59.860186144Z at com.liferay.portal.file.install.internal.DirectoryWatcher._startBundles(DirectoryWatcher.java:1189) [bundleFile:?]
2022-07-22T07:32:59.860187736Z at com.liferay.portal.file.install.internal.DirectoryWatcher._process(DirectoryWatcher.java:1046) [bundleFile:?]
2022-07-22T07:32:59.860189692Z at com.liferay.portal.file.install.internal.DirectoryWatcher.run(DirectoryWatcher.java:221) [bundleFile:?]
But the fragment doesn't give you the same errors. In fact Liferay doesn't give you any message in such case.
The issue is that the DirectoryWatcher which is responsible for starting modules works differently for fragments and for other modules. The fragments modules as you might now cannot be started, they can only be resolved (meaning all dependencies have been met) and then they works.
Then how can we find out that the fragment has not resolved?
Of course we still can find out that the fragment has not been resolved by using gogo shell. We can use the
lb
command which lists all the bundle. Then we can find bundles which are in "Installed" state and not resolved or active.
Using this method we can find our two bundles:
Tip In the Gogo Shell we can also use grep to filter the results:
lb | grep "Installed"
Then we can use the diag
command to find out why particular bundle hasn't started:
The main issue I have with that approach is that you have to manully use lb
.
In fact there is even an OSGi command called system:check
which is supposed to find all the issues in the deployed bundles
but it just doesn't show you any issues in our scenario:
And this is what we're going to change
System Checkers
What system checkers are
System checkers are the components which do different checks of your OSGi bundles.
The idea is that you can run system:check
and find out if everything is fine.
Also the system check is run automatically on every startup if you have a following portal properties entry:
module.framework.properties.initial.system.check.enabled=true
It works quite well for normal modules but like shown before you can't fully rely on the default system checkers if you're using fragments.
How we can implement a custom checker - example of creating one which finds installed (but not resolved) bundles
So we know that the default system checkers are not reliable in all situations but we can fix that with custom system checker which we will create on our own. The idea is really simple. We have to:
- Create a class implementing SystemChecker interface
- Register our class as a component
- Implement all methods from the interface
So lets start with an empty class:
public class UnresolvedBundlesSystemChecker implements SystemChecker {
}
Then add a Component
annotation:
@Component(
immediate = true,
property = {},
service = SystemChecker.class
)
public class UnresolvedBundlesSystemChecker implements SystemChecker {
}
This interface has three methods we have to implement
public interface SystemChecker {
public String check();
public String getName();
public String getOSGiCommand();
}
Lets start with the second and third ones. The getName
one is supposed to just return name of the checker:
@Override
public String getName() {
return "Unresolved Requirement System Checker";
}
The getOSGiCommand
should return a OSGi command which can be run to execute this system check on its own.
This command name is later logged in the system:check
result.
@Override
public String getOSGiCommand() {
return "pydyniak:unresolved";
}
You can name the command as you want. I will show you the implementation of the command component later.
Now lets go to the check method. This is the method that is being called when you run the system:check
and it's the one where you should implement all your logic. In the end you have to return a String which will be displayed by
system:check
but of course you can add some extra logic there for example use Log
to also log the issues to the log files.
How the logic should look like in our example? Well lets start with the fact that you can get information about bundles in OSGi by using BundleContext which you can obtain in the activate method of your component. So lets get the BundleContext and save it:
private BundleContext bundleContext;
@Activate
protected void activate(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
Once we have that we can use that in our check
method. I will first create an Util class which will handle our logic:
public class UnresolvedBundlesUtil {
private UnresolvedBundlesUtil(){}
public static String listUnresolvedBundles(BundleContext bundleContext) {
return Arrays.stream(bundleContext.getBundles())
.filter(UnresolvedBundlesUtil::isInInstalledState)
.map(UnresolvedBundlesUtil::getMessage)
.collect(Collectors.joining());
}
/**
* We only look for installed bundles. This is the states in which bundles might have unresolved requirements
* @param bundle
* @return
*/
private static boolean isInInstalledState(Bundle bundle) {
return Bundle.INSTALLED == bundle.getState();
}
private static String getMessage(Bundle bundle) {
StringBuilder message = new StringBuilder();
message.append("\n\tBundle {id: ");
message.append(bundle.getBundleId());
message.append(", symbolicName: ");
message.append(bundle.getSymbolicName());
message.append("} is unresolved");
return message.toString();
}
}
So what are we doing here - the listUnresolvedBundles method take BundleContext
object as a parameter. Then
it uses it to obtain all the bundles in your OSGi container. In our example we want to find bundles that are not resolved.
In OSGi world the bundle which couldn't be resolved is a bundle for which not all dependencies have been met.
In such scenario it stays in Installed
state. When new bundles are deployed our installed bundles are checked again in order to verify if the requirements have been met or not.
If yes then the installed bundle gets resolved as well.
Note if you're not familiar with OSGi states you can read some more about them for example in JavaDocs of Bundle or in Liferay docs (they have some nice pictures) or in many different places across the Internet.
Checking the bundle state is quite easy and can be seen in isInInstalledState
method:
private static boolean isInInstalledState(Bundle bundle) {
return Bundle.INSTALLED == bundle.getState();
}
Then once we find bundles that are installed we can create some basic message for each of them:
private static String getMessage(Bundle bundle) {
StringBuilder message = new StringBuilder();
message.append("\n\tBundle {id: ");
message.append(bundle.getBundleId());
message.append(", symbolicName: ");
message.append(bundle.getSymbolicName());
message.append("} is unresolved");
return message.toString();
}
Both methods mentioned above are used in the listUnresolvedBundles
which uses Lambda to do all the work:
public static String listUnresolvedBundles(BundleContext bundleContext) {
return Arrays.stream(bundleContext.getBundles())
.filter(UnresolvedBundlesUtil::isInInstalledState)
.map(UnresolvedBundlesUtil::getMessage)
.collect(Collectors.joining());
}
Going back to our check
method in UnresolvedBundlesSystemChecker
class - we simply call our listUnresolvedBundles
method
and return the result:
@Override
public String check() {
return UnresolvedBundlesUtil.listUnresolvedBundles(bundleContext);
}
Creating a OSGi command
In the beginning we defined custom OSGi command called pydyniak:unresolved
which can be executed to run the system checker on its own.
Sadly this won't call our check
method by default. Luckily we can easily create the OSGi command on our own.
To do that we have to create a simple component which again calls our UnresolvedBundlesUtil
class and then simply
uses System.out.println
to log the result in the gogo shell:
@Component(
immediate = true,
property = {"osgi.command.function=unresolved", "osgi.command.scope=pydyniak"},
service = UnresolvedBundlesOSGICommand.class
)
public class UnresolvedBundlesOSGICommand {
private BundleContext bundleContext;
@Activate
protected void activate(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
public void unresolved() {
System.out.println(UnresolvedBundlesUtil.listUnresolvedBundles(bundleContext));
}
}
If you have created OSGi commands before then it should be simple. If not then it's enough to understand that the
osgi.command.function
and osgi.command.scope
define how our command can be run and then we just need a function
named exactly the same as the osgi.command.function
in our case it's unresolved
.
Testing
Once we have everything we can get to the testing. First we must deploy our modules. Then we should go to the
Control Panel -> Gogo Shell
and execute system:check
command (it takes a while). In the result we can see our custom
system checker being run and its result:
We can also test our custom OSGi command, which is mentioned in the system:check
result:
Now we can be sure that our code works just fine.
Final thoughts
With this quite simple module we can be more certain that if we run system:check
we get all information about the errors.
As far as I'm concerned there are no other possible errors in bundles which could not be found with system:check
(but I might be wrong ;)).
System checkers of course could be use for different things, even not related to the bundles or components states. Same for OSGi command - it's really cool feature in my opinion which I have used in few occasions and I really like them. They're quite well documented so if you have any doubts you can read the Liferay documentation.
On the other hand, the System Checkers are not documented at all, at least not for the moment. You can only find information about how to run the system check but not how to implement custom ones. That's why I decided to create this blog post.
The whole example (API, Service, Custom Portlet and Fragment) can be found on my Github: https://github.com/RafalPydyniak/liferay-blog-samples/tree/master/modules/not-resolved-bundles
Of course feel free to use the code in any way you want.
Like always I hope you learned something and of course if you have any questions just email me.