Adding Dependencies to JSP Fragment Bundles

Recently I was lamenting how I felt that JSP fragment bundles could not introduce new dependencies and therefore the JSP overrides could really not do much more than reorganize or add/remove already supported elements on the page.

For me, this is like only 5% of the use cases for a JSP override. I am much more likely to need to add new functionality that the original portlet developers didn't need to consider.  I need to be able to add new services and use those in the JSP to retrieve entities, and sometimes just really do completely different things w/ the JSP that perhaps were never imagined.

The first time I tried a JSP override to do something similar with a JSP fragment bundle, I was disappointed. My fragment bundle would get to status "Installed" in GoGo, but would go no further because it had unresolved references.  It just couldn't get to the resolved stage.

How could I make the next great JSP fragment override bundle if I couldn't access anything outside the original set of services?

My good friend and coworker Milen Dyankov heard my rant and offered the following insight:

According to the spec:

... requirements and capabilities in a fragment bundle never become part of the fragment's Bundle Wiring; they are treated as part of the host's requirements and capabilities when the fragment is attached to that host.

As for providing declarative services in fragments, again the spec is clear:

A Service-Component manifest header specified in a fragment is ignored by SCR. However, XML documents referenced by a bundle's Service-Component manifest header may be contained in attached fragments.

In another words if your host has Service-Components: OSGI-INF/*.xml then your fragment can put a new XML file in OSGI-INF folder and it will be processed by SCR.

Now sometimes Milen seems to forget that I'm just a mere mortal and not the OSGi guru he is, so while this was perfectly clear to him, it left me wondering if there was anything here that would be my lever to lift the lid and peek inside the JSP fragment bundle realm.

The remainder of this blog is the result of that epic journey cheeky.

Service Component Runtime

The SCR is the Apache Felix implementation of the OSGi Declarative Services specification. It's responsible for handling the service registry and lifecycle management of DS components within the OSGi container, starting/stopping the services as bundles are started/stopped, wiring up @Reference dependencies in DS components, etc.

Since the fragment bundle handling comes from the Apache Felix implementation, it's not really a Liferay component and certainly not one that would lend itself to an override in the normal Liferay sense. Anything we do here to access services in the JSP fragment bundles is going to have to go through supported OSGi mechanisms or we won't get anywhere.

So the key for Milen's quote above is the "XML documents referenced by a bundle's Service Component manifest header may be contained in attached fragments." The rough translation here - we might be able to provide an override XML file for one of the bundle host's components and possibly inject new dependencies. Yes, as a rough translation it really assumes that you know more than what what you might (and especially more that what I did), so let's divert for a second.

Service Component Manifest XML Documents

So the BND tool that we all know and love, that guy actually does many, many things for us when it builds a bundle jar. One of those tasks is to generate the service component manifest and all of the XML documents. The contents of all of these files is basically the metadata the SCR will need for dependency resolution, component wiring, etc.

Any time you annotate your java class with @Component you are indicating it is a DS service. When BND is processing the annotations, it's going to add an entry to the Service Component Manifest (so the SCR will process the component during bundle start). The Service Component Manifest is the Service-Component key in the bundle's MANIFEST.MF file, and it lists each individual XML file in the OSGI-INF, one for each component.

These XML files define the component for the SCR, specifying the java class that implements the component, the service it provides, all reference details and properties for the component.

So if you take any bundle jar you have, expand it and check out the MANIFEST.MF file and look for the Service-Component key. You'll find there's one OSGI-INF/com.example.package.JavaClass.xml file (where it is your package and class) for each component defined in your bundle.

If you open one of the XML files, you can see the structure for a component definition, and it is easy to see how things that you set in the @Component annotation attributes have been mapped into the XML file.

Now that we know about the manifest and XML docs, we can get back to our regularly scheduled program.

Overriding An SCR XML

So remember, we should be able to override one of these files because "XML documents referenced by a bundle's Service Component manifest header may be contained in attached fragments."

This hints that we cannot add a new file, but we could override an existing one.

So to me, this is the key question - can we create an override XML file to introduce a new dependency, one that really cannot be directly bound to the original (since we can't modify the class) so at least the bundle would have a new dependency and the JSP would be happy?

Well I actually used all of this newfound knowledge to work up a test and tried it out, but it fails. It didn't make any sense...

Return To The Jedi

"Milen, my SCR XML override isn't working."

"Overrides won't work because the XML files are loaded by the class loader, and the host bundle comes before the fragment bundle so SCR ignores the override.  You can't override the XML, you can only add a new one to the fragment bundle."

"But Milen you said I couldn't add new XML files, only those listed in the Service-Component in the MANIFEST.MF file of the host bundle will be used by SCR during loads."

"Change your Service-Component key to use a wildcard like OSGI-INF/* and SCR will load the ones from the host bundle as well as the fragment bundle. It's considered bad practice, but it would work."

"I can't do that, Milen, I'm doing a JSP fragment bundle on a Liferay host bundle, I can't change the Service-Component manifest value and, if I could, I wouldn't need to do any of this fragment bundling in the first place because I would just apply my change directly in the host bundle and be done with it."

"Well then the SCR XML override isn't going to work. Let's try something else..."

Example Project

After working out a new plan of attack, I was going to need an example project to test this all out and verify that it was going to work. The example must include a JSP fragment bundle override and introduce another previously unused service. I don't really want to do any more coding than necessary here, so let's pick something to do out of the portal JSPs and services.

Requirement: On login form, display the current count of membership requests.

Pretty simple, maybe part of some automated membership request handling being added to the portal or trying to show how popular the site is by showing count of how many are waiting to get in.

But it gives us the goal here, we want to access the MemberRequestLocalService inside of the login.jsp page of the login-web host bundle. The service is defined in the com.liferay.invitation.invite.members.api bundle and is not currently connected in any way with the login web module.

Creating The Fragment Bundle

I'll continue my pattern of using blade on the command line, but of course you're free to leverage tools provided by your IDE.

blade create -t fragment -h com.liferay.login.web -H 1.1.4 login-web-fragment

Remember to choose the fragment bundle version from your local portal so you'll override the right one and make OSGi/SCR happy.

Copy in the login.jsp page from the portal source. After the include of init.jsp, add the following lines:

<%@ page import="com.liferay.invitation.invite.members.service.MemberRequestLocalService" %>

<%
  // get the service from the render request attributes
  MemberRequestLocalService memberRequestLocalService = (MemberRequestLocalService)
    renderRequest.getAttribute("MemberRequestLocalService");
	
  // get the current count
  int currentRequestCount = memberRequestLocalService.getMemberRequestsCount();
	
  // display it somewhere on the page...
%>

Very simple. Doesn't really display, but that's not the point in this blog.

Now if you build and deploy this guy as-is, if you check him you'll see his state in GoGo is "Installed". This is not good as it is not where it needs to be for the JSP fragment to work.

Adding The Dependency

So we have to go back to how the OSGi handles the fragment bundles... So when OSGi is loading the fragment, effectively the MANIFEST.MF items from the fragment bundle will be merged with those from the host bundle.

For me, that means I have to list my dependency in build.gradle and trust BND will add the right Import-Package declaration to the final MANIFEST.MF file.

Then, when the framework is loading my fragment bundle, my Import-Package from the fragment will be added to the Import-Package of the host bundle and all should be good.

JSP fragment bundles created by blade do not have dependencies listed in the build.gradle file (in fact it is completely empty), so let's add the dependency stanza:

dependencies {
  compile group: "com.liferay", name: "com.liferay.invitation.invite.members.api", version: "2.1.1"
}

We only need to add the dependency that is missing from the host bundle, the one with the service we're going to pull in.

After building, you can unpack the jar and check the MANIFEST.MF file and see that it does now have the Import-Package declaration, so if SCR does actually do the merge while loading, we should be in business.

Deploy your new JSP fragment bundle and if you check the bundle status in GoGo, you'll see it is now "Resolved".

Sweet!

Injecting The Reference

Not so fast. If you try to log into your portal, you'll get the "portlet is temporarily unavailable" message and the log file will have a NullPointerException and a big stack trace. We've totally broken the login portlet because login.jsp depends upon the service but it is not set.

If you check the JSP change I shared, I'm pulling the service instance from the render request attributes. But how the heck does it get in there when we cannot change the host bundle to inject it in the first place?

We're going to do this using another OSGi module with a new component that implements the PortletFilter interface, specifically a RenderFilter.

@Component(
  immediate = true,
  property = {
      "javax.portlet.name=" + LoginPortletKeys.LOGIN,
      "javax.portlet.name=" + LoginPortletKeys.FAST_LOGIN
  },
  service = PortletFilter.class
)
public class LoginRenderFilter implements RenderFilter {
  @Override
  public void doFilter(RenderRequest request, RenderResponse response, FilterChain chain) throws IOException, PortletException {
    // set the request attribute so it is available when the JSP renders
    request.setAttribute("MemberRequestLocalService", _memberRequestLocalService);

    // let the filter chain do it's thing
    chain.doFilter(request, response);
  }

  @Override
  public void init(FilterConfig filterConfig) throws PortletException { }

  @Override
  public void destroy() { }

  @Reference(unbind = "-")
  protected void setMemberRequestLocalService(final MemberRequestLocalService memberRequestLocalService) {
    _memberRequestLocalService = memberRequestLocalService;
  }

  private MemberRequestLocalService _memberRequestLocalService;
}

So here we are intercepting the render request using the portlet filter. We inject the service into the request attributes before invoking the filter chain to complete the rendering; that way when the JSP page from the fragment bundle is used, the attribute will be set and ready.

Build and deploy your new component. Once it starts, refresh your browser and try to log in. You should now see the login portlet again. Not that we did anything fancy here, we're just proving that the service reference is not null and is available for the JSP override to use.

Conclusion

So we took a roundabout path to get here, but we've seen how we can create a JSP fragment bundle to override portal JSPs, add a dependency to the fragment bundle that gets included as a dependency in the host bundle, and we created a portlet filter bundle to inject the service reference in the request attributes so it would be available to the JSP page.

Two different bundle jars, but it certainly gets the job done.

Also along the way we learned some things about what the SCR is, how fragment bundles work, as well as some of the internals of our OSGi bundle jars and the role that BND plays in their construction.  Useful information, IMHO, that can help you while learning Liferay 7 CE/Liferay DXP.

This now opens some new paths for you to pursue for your JSP fragment bundles.  Just follow the outline here and you should be good to go.

Find the project code for the blog here: https://github.com/dnebing/jsp-fragment

Blogs
This works for services fine (e.g. if we have API), but what about the modules which are dedicated to be ResourceLoader holders? I have a module which contains the class which implements ResourceBundleLoader, and a Language.properties file. Let this module be named as com.example.registration.resource. How would you add it as a dependency to the fragment using it?
Interesting technique for injecting reference to services.
But it's not really about dependencies: I tried to simply use MemberRequestLocalServiceUtil and it just works...
Yes, Outside using the Util class for Services. Other components are only accessible using OSGi service registry. one practical example is using Item Selector in your fragment.

hi all,

is this still valid for 7.1?

Yes, as well as for 7.2.

Hi David, thanks for the blog post. I'm trying to use this approach, and it works fine with my custom service, but only if the service does not have any custom finder defined. Otherwise, the service object I'm pushing as a request attribute is null. Do you know if there is a workaround for this?

 

Thanks!