Today, I faced an issue with so-called "Factory configurations" in Liferay, which are a way of defining custom configurations with multiple instances. I could not find any documentation or examples online, so I thought I would share the solution in this short article.
Custom configurations in Liferay
As you might or might not know, in Liferay, you can create custom configurations by defining a simple interface and using a few out-of-the-box annotations. What is cool about this is that the whole UI gets generated for you, so you do not have to deal with any portlets, JSPs, React portlets, etc. Another cool thing is that you can choose the scope of your configuration (system, instance aka company, site, portlet).
A basic example of such a configuration could be the following code:
package com.pydyniak.custom.configurations;
import aQute.bnd.annotation.metatype.Meta;
import com.liferay.portal.configuration.metatype.annotations.ExtendedObjectClassDefinition;
@Meta.OCD(
id = "com.pydyniak.custom.configurations.CustomConfiguration",
name = "My cool custom configuration"
)
@ExtendedObjectClassDefinition(
category = "pydyniak",
scope = ExtendedObjectClassDefinition.Scope.COMPANY
)
public interface CustomConfiguration {
@Meta.AD(name = "My custom Config", description = "This is just an example configuration")
String myCustomConfig();
}
which would result in the following configuration under Control Panel -> Instance Settings:
Using such a configuration is easy. We can just use the ConfigurationProvider OSGi service or ConfigurationProviderUtil, for example:
public CustomConfiguration getConfiguration(long companyId) throws ConfigurationException {
return ConfigurationProviderUtil.getCompanyConfiguration(CustomConfiguration.class, companyId);
}
This is quite simple and straightforward. There are also a bunch of examples in the Liferay code (just look for the Configuration.java suffix to find them easily), and it is quite well described in the Liferay docs, so I won't go into more details.
Factory configurations
One thing that is kind of missing in configurations and is not widely used is the so-called "Factory Configuration". The factory configuration is a way of creating one configuration class, very similar to the one presented, but with extra options that make it possible to have multiple "instances" of this configuration. One example from Liferay is CORS settings, where you can define multiple entries:
Doing that is really simple: in @Meta.OCD
you have to use factory=true
option and in @ExtendedObjectClassDefinition
you need to define which field is used as a name in list view using factoryInstanceLabelAttribute = "CONFIGURATION_METHOD_NAME"
where CONFIGURATION_METHOD_NAME
is just a name of method (field) from your configuration.
A full example could be a configuration where we want to configure multiple SSO partners with different settings (whatever they might be),
and this was exactly the case I needed:
package com.pydyniak.custom.configurations;
import aQute.bnd.annotation.metatype.Meta;
import com.liferay.portal.configuration.metatype.annotations.ExtendedObjectClassDefinition;
@Meta.OCD(
factory = true,
id = "com.pydyniak.custom.configurations.SSOPartnerConfiguration",
name = "SSO Partner Login Token"
)
@ExtendedObjectClassDefinition(
category = "pydyniak",
factoryInstanceLabelAttribute = "ssoPartnerName",
scope = ExtendedObjectClassDefinition.Scope.COMPANY
)
public interface SSOPartnerConfiguration {
@Meta.AD(required = false, name = "Enabled", deflt = "false",
description = "Should SSO be enabled for this partner")
boolean enabled();
@Meta.AD(name = "SSO Partner Name")
String ssoPartnerName();
}
The example above, of course simplified, shows how to create a factory configuration. In the portal, this would look like this: and creating a new SSO partner: and once we create some SSO partners, we have:
Doing that was quite easy. I could easily find examples in the Liferay code, such as the CORS configuration mentioned earlier: PortalCORSConfiguration
, or the OpenID SSO configuration in OpenIdConnectProviderConfiguration
.
What I could not find easily, though, was the way of accessing these configurations from code.
Accessing factory configurations in code
After some research I found a way of doing that - first of all we need to use ConfigurationAdmin service
@Reference
private ConfigurationAdmin configurationAdmin;
With this service, we can list configurations using the listConfigurations method.
It takes a filter as a parameter, and what would it be? Well, looking at the sources, I found out that it has to have a format like:
(&(service.factoryPid=CONFIGURATION_CLASS)(companyId=COMPANY_ID))
where:
CONFIGURATION_CLASS
is just the class name of our configuration. In my example, it is: SSOPartnerConfiguration.class.getName()COMPANY_ID
is simply the company (instance) ID, as my configuration is company-scoped.
When we use that method we get array of org.osgi.service.cm.Configuration
which is generally ok - we can get our
properties out of it.
We can go one step further, though, and find the configuration we need and convert it to our SSOPartnerConfiguration
.
To do that, we can combine the ConfigurableUtil#createConfigurable
method with the Configuration#getProperties
.
A full example code would be:
@Reference
private ConfigurationAdmin configurationAdmin;
private Optional<SSOPartnerConfiguration> getSsoPartnerConfiguration(long companyId, String ssoPartner) throws IOException, InvalidSyntaxException {
Configuration[] configurations = configurationAdmin.listConfigurations(String.format("(&(service.factoryPid=%s)(companyId=%s))", SSOPartnerConfiguration.class.getName(), companyId));
return Arrays.stream(configurations).map(Configuration::getProperties)
.map(properties -> ConfigurableUtil.createConfigurable(SSOPartnerConfiguration.class, properties))
.filter(ssoPartnerConfig -> ssoPartnerConfig.ssoPartnerId().equals(ssoPartner))
.findAny();
}
Summary
I hope this short article can help you with your development. It would definitely have saved me some time if I had found it in Google or in Liferay docs.
The configuration options are well described in Liferay docs, so I suggest you read through them if you are not familiar with the functionality. It's just this one piece of information that was missing; hopefully, this article is easy enough to follow.
As always, if you have any questions, do not hesitate to contact me.
Also, you can find related code on my Github.