Liferay: Factory Configurations

By Rafał Pydyniak | 2024-06-19

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:

Custom Config
Custom Config

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:

Cors Settings
Cors Settings

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:

SSO Partners configuration
SSO Partners configuration
and creating a new SSO partner:
Adding new SSO Partner
Adding new SSO Partner
and once we create some SSO partners, we have:
List of SSO partners, after adding some
List of SSO partners, after adding some

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.

Copyright: Rafał Pydyniak