Blogs
How to handle waiting at startup for data to be available using a custom ModuleServiceLifecycle event...
Introduction
A community member recently posted a question about how to make their component wait. I of course pointed them at my Power of Patience blog which I have been using for years now to wait for the portal to be ready.
They came back and said it didn't work, which seemed quite odd to me because I and many others have been very successful with it.
After digging a little deeper though, it quickly became clear why it wasn't working. I came up with a cool solution and thought I'd share it here...
Why Waiting on Portal Ready Wasn't Enough
So effectively the community member wrote an
UpgradeProcess
implementation that was supposed to add
resource permissions permissions to a role they created.
Now if they deployed this to an already running Liferay instance, all of this was fine.
However, if this was deployed prior to launching Liferay the first time, they got an exception:
ERROR [main][CustomUpgradeProcess:355] Upgrade process stopped because of issue : com_liferay_roles_admin_web_portlet_RolesAdminPortlet#VIEW com.liferay.portal.kernel.exception.NoSuchResourceActionException: com_liferay_roles_admin_web_portlet_RolesAdminPortlet#VIEW at com.liferay.portal.service.impl.ResourceActionLocalServiceImpl .getResourceAction(ResourceActionLocalServiceImpl.java:324) at jdk.internal.reflect.GeneratedMethodAccessor261.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at com.liferay.portal.spring.aop.AopMethodInvocationImpl .proceed(AopMethodInvocationImpl.java:41) at com.liferay.portal.spring.aop.AopInvocationHandler .invoke(AopInvocationHandler.java:40) at com.sun.proxy.$Proxy69.getResourceAction(Unknown Source) at com.liferay.portal.service.impl.ResourcePermissionLocalServiceImpl ._updateResourcePermission(ResourcePermissionLocalServiceImpl.java:2263) at com.liferay.portal.service.impl.ResourcePermissionLocalServiceImpl .updateResourcePermission(ResourcePermissionLocalServiceImpl.java:1745) at com.liferay.portal.service.impl.ResourcePermissionLocalServiceImpl .addResourcePermission(ResourcePermissionLocalServiceImpl.java:311)
Basically, at the point when their UpgradeProcess
was executing, it expected that the roles-admin-web portlet module
was done deploying, including processing the
resource-actions/default.xml
file where the
ResourceActions
were defined.
In an already running portal, this is a fine expectation to make because Liferay would have processed that during the first launch.
But when you do this while Liferay is starting the first time, there isn't really a way in Liferay to wait on data, only for other services or components.
Waiting for Data
So what the community member needed was a way to [possibly] wait for the data to be available.
There's two scenarios to take into account:
- The portal has already started and the data is already available, we are good to proceed immediately.
- The portal is launching the first time, the data isn't yet available, but when it is added we are good to go.
So I actually came up with a solution for this, but I started
from item #2 - how do we know when data is added? Well, a
ModelListener
can do this for us...
So I started my new ModelListener
implementation:
@Component( immediate = true, service = ModelListener.class ) public class RoleAdminPortletViewResourceActionModelListener extends BaseModelListener<ResourceAction> implements ModelListener<ResourceAction> { @Override public void onAfterCreate(ResourceAction model) throws ModelListenerException { if (model.getName().equals(RolesAdminPortletKeys.ROLES_ADMIN) && (model.getActionId().equals(ActionKeys.VIEW))) { // our resource action has been created... } } }
This will tell me when the resource action is available, so now
I need to notify it is ready. I decided that I'd borrow a page from
Liferay and use a custom ModuleServiceLifecycle
, so I
would need to register a new service:
private BundleContext _bundleContext; private ServiceRegistration<ModuleServiceLifecycle> _serviceRegistration = null; @Activate protected void activate(BundleContext bundleContext) { _bundleContext = bundleContext; } protected void registerResourceActionAvailable() { if (_serviceRegistration != null) return; _serviceRegistration = _bundleContext.registerService( ModuleServiceLifecycle.class, new ModuleServiceLifecycle() { }, HashMapDictionaryBuilder.<String, Object> put("module.service.lifecycle", "roles.admin.view.resource.action.available") .put("service.vendor", ReleaseInfo.getVendor()) .put("service.version", ReleaseInfo.getVersion()) .build() ); } @Deactivate protected void deactivate() { if (_serviceRegistration == null) return; _serviceRegistration.unregister(); _serviceRegistration = null; }
With this code ready, I tweaked the onAfterCreate()
method just a bit:
@Override public void onAfterCreate(ResourceAction model) throws ModelListenerException { if (_serviceRegistration != null) { // already found the data, nothing to do here return; } if (model.getName().equals(RolesAdminPortletKeys.ROLES_ADMIN) && (model.getActionId().equals(ActionKeys.VIEW))) { // our resource action has been created... registerResourceActionAvailable(); } }
So now, when the data was added, my new
ModuleServiceLifecycle
instance would be registered and
I'd know the data was ready, so I've completely handled case #2.
All that was remaining was case #1, and I solved that with a
change to my activate()
method:
@Reference private ResourceActionLocalService _resourceActionLocalService; @Activate protected void activate(BundleContext bundleContext) { _bundleContext = bundleContext; ResourceAction rsrcAction = _resourceActionLocalService .fetchResourceAction(RolesAdminPortletKeys.ROLES_ADMIN, ActionKeys.VIEW); if (rsrcAction != null) { // data already exists registerResourceActionAvailable(); } }
In the activate()
method I just do a quick check to
see if the resource action has already been added. If it has, I know I
can immediately register as available, and this covers case #1.
Listening for the Lifecycle Event
So this part is very much like the Power
of Patience blog, except I'm waiting on a custom
ModuleServiceLifecycle
, so it is just a tiny bit more verbose.
In the UpgradeProcess
implementation, adding the
following will wait for the availability of the data:
@Reference( target = "(module.service.lifecycle=roles.admin.view.resource.action.available)", unbind = "-") protected void setModuleServiceLifecycle( ModuleServiceLifecycle moduleServiceLifecycle) { }
Conclusion
So as in the Power
of Patience blog, we can use a
ModuleServiceLifecycle
to pause the activation of our
component until the service has been registered, and here we're using
a custom service and a custom trigger (the ModelLIstener
)
to register the service.
Granted having to wait for a specific piece of portal data is a unique case, one that should not happen often.
More often than not, waiting for PORTAL_INITIALIZED
is going to be enough for most of your delaying tactics.
However, there are cases where you may want to wait for some sort of event (i.e. connecting to a remote database, receiving data from a remote web service, a file available on the filesystem, etc) before one of your components can do its thing.
The pattern I've laid out here, registering a custom
ModuleServiceLifecycle
and then @Reference
it is one that you can use to support this kind of waiting.