May 3, 2020
Estimated Post Reading Time ~

Programmatically updating OSGi configurations in AEM and Sling

OSGi components can be configured via the concept of Managed Services and Managed Service Factories. Each component has a Service PID. Once a configuration has been updated, the changes are persisted to the file system. In Adobe Experience Manager you can find the files under the crx-quickstart/launchpad/config folder. If you're using the Sling jar, you can find the files under the sling/config folder.

Hopefully you're following best practices and creating sling:OsgiConfig files so that configurations can be stored in your version control system and configured for specific server instances assigned by Sling run modes. When you want to manually update an OSGi configuration you do so through the Apache Felix console's Configuration screen located at http://localhost:4502/system/console/configMgr.

You can programmatically read and update OSGi configurations through the Apache Felix provided ConfigurationAdmin service. In fact, this is the same service used by the Apache Felix console. When you look at the Configuration console it tells you the status of the ConfigurationAdmin service. If you disable the Apache Felix Configuration Admin Service (org.apache.felix.configadmin) you will notice that you are no longer able to update configurations from the Felix console.

Using the service is simple and straightforward. However, the one thing to watch out for is that updating the configuration though ConfigurationAdmin is an asynchronous call and the component associated with the configuration will become unavailable for a brief moment on update. That means trying to make rapid consecutive updates or trying to read from the configuration directly after an update is unreliable unless properly handled.

The following example demonstrates how to get a Configuration from a Managed Service as well as a Managed Service Factory, how to get properties from the configuration, and how to update the configuration.

OsgiConfigExampleServlet.java

package com.nateyolles.aem;

import org.osgi.framework.InvalidSyntaxException;
import org.osgi.service.cm.Configuration;

import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.service.cm.ConfigurationAdmin;

import javax.servlet.ServletException;

import java.io.IOException;
import java.util.Dictionary;
import java.util.Hashtable;

@SlingServlet(paths = "/bin/bar")
public class OsgiConfigExampleServlet extends SlingAllMethodsServlet {

    /** Service to get OSGi configurations */
    @Reference
    private ConfigurationAdmin configAdmin;

    private static final String LOGGER_FACTORY_PID = "org.apache.sling.commons.log.LogManager.factory.config";
    private static final String CUSTOM_LOGGER_PID = "org.apache.sling.commons.log.LogManager.factory.config.278b07c1-7eb9-43a8-8d84-8fed5ee6b0a4";
    private static final String HTML_LIBRARY_MANAGER_PID = "com.day.cq.widget.impl.HtmlLibraryManagerImpl";
    private static final String MINIFY_PROPERTY = "htmllibmanager.minify";

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
            throws ServletException, IOException {

        /* Managed Service and Manage Service Factory configs */
        Configuration loggerFactoryConfig = configAdmin.getConfiguration(LOGGER_FACTORY_PID);
        Configuration loggerConfig = configAdmin.getConfiguration(CUSTOM_LOGGER_PID);

        /* returns true */
        boolean isFactoryPid = loggerConfig.getFactoryPid().equals(loggerFactoryConfig.getPid());

        /* Get all configs from the factory */
        try {
            /* Java filter syntax*/
            String filter = '(' + ConfigurationAdmin.SERVICE_FACTORYPID + '=' + LOGGER_FACTORY_PID + ')';
            Configuration[] allLoggerConfigs = configAdmin.listConfigurations(filter);
        } catch (InvalidSyntaxException e) {
            // TODO Auto-generated catch block
        }
        /*********************************************/


        /* Get properties */
        Configuration htmlLibraryManangerConfig = configAdmin.getConfiguration(HTML_LIBRARY_MANAGER_PID);
        Dictionary<String, Object> properties = htmlLibraryManangerConfig.getProperties();

        boolean isMinify = PropertiesUtil.toBoolean(properties.get(MINIFY_PROPERTY), false);
        /*********************************************/


        /* Set properties */
        if (properties == null) {
            properties = new Hashtable<String, Object>();
        }

        /* Remember HashTables don't accept null values. */
        properties.put(MINIFY_PROPERTY, true);
        htmlLibraryManangerConfig.update(properties);
    }
}

The files below are a service I created for the Publick Sling + Sightly Blog Engine to manage configurations easily and consistently throughout the application as well as a sample consumer of the service.

OsgiConfigurationServiceImpl.java

package com.nateyolles.sling.publick.services.impl;

import java.io.IOException;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.osgi.PropertiesUtil;

import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.nateyolles.sling.publick.services.OsgiConfigurationService;

/**
 * Service to interact with OSGi configurations.
 */
@Service(value = OsgiConfigurationService.class)
@Component(name = "Publick OSGi Configuration Service",
           description = "Programatically set properties of OSGi configurations.")
public class OsgiConfigurationServiceImpl implements OsgiConfigurationService {

    /** The service to get OSGi configs */
    @Reference
    private ConfigurationAdmin configAdmin;

    /** The logger */
    private static final Logger LOGGER = LoggerFactory.getLogger(OsgiConfigurationServiceImpl.class);

    /**
     * Set the value of an OSGi configuration property for a given PID.
     *
     * @param pid The PID of the OSGi component to update
     * @param property The property of the config to update
     * @param value The value to assign the provided property
     * @return true if the property was updated successfully
     */
    public boolean setProperty(final String pid, final String property, final Object value) {
        try {
            Configuration conf = configAdmin.getConfiguration(pid);

            @SuppressWarnings("unchecked")
            Dictionary<String, Object> props = conf.getProperties();

            if (props == null) {
                props = new Hashtable<String, Object>();
            }

            props.put(property, value != null ? value : StringUtils.EMPTY);
            conf.update(props);
        } catch (IOException e) {
            LOGGER.error("Could not set property", e);
            return false;
        }

        return true;
    }

    /**
     * Set the values of an OSGi configuration for a given PID.
     *
     * @param pid The PID of the OSGi component to update
     * @param properties The properties and values of the config to update
     * @return true if the properties were updated successfully
     */
    public boolean setProperties(final String pid, final Map<String, Object> properties) {
        try {
            Configuration conf = configAdmin.getConfiguration(pid);

            @SuppressWarnings("unchecked")
            Dictionary<String, Object> props = conf.getProperties();

            if (props == null) {
                props = new Hashtable<String, Object>();
            }

            /* 
             * props is of type org.apache.felix.cm.impl.CaseInsensitiveDictionary which
             * contains an internal HashTable and doesn't contain a putAll(Map) method.
             * Iterate over the map and put the values into the Dictionary individually.
             * Remove null values from HashMap as HashTable doesn't support them.
             */
            for (Map.Entry<String, Object> entry : properties.entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();

                props.put(key, value != null ? value : StringUtils.EMPTY);
            }

            conf.update(props);
        } catch (IOException e) {
            LOGGER.error("Could not set property", e);
            return false;
        }

        return true;
    }

    /**
     * Get the value of an OSGi configuration string property for a given PID.
     *
     * @param pid The PID of the OSGi component to retrieve
     * @param property The property of the config to retrieve
     * @param value The value to assign the provided property
     * @return The property value
     */
    public String getProperty(final String pid, final String property, final String defaultValue) {
        try {
            Configuration conf = configAdmin.getConfiguration(pid);

            @SuppressWarnings("unchecked")
            Dictionary<String, Object> props = conf.getProperties();

            if (props != null) {
                return PropertiesUtil.toString(props.get(property), defaultValue);
            }
        } catch (IOException e) {
            LOGGER.error("Could not get property", e);
        }

        return defaultValue;
    }

    /**
     * Get the value of an OSGi configuration boolean property for a given PID.
     *
     * @param pid The PID of the OSGi component to retrieve
     * @param property The property of the config to retrieve
     * @param value The value to assign the provided property
     * @return The property value
     */
    public boolean getProperty(final String pid, final String property, final boolean defaultValue) {
        try {
            Configuration conf = configAdmin.getConfiguration(pid);

            @SuppressWarnings("unchecked")
            Dictionary<String, Object> props = conf.getProperties();

            if (props != null) {
                return PropertiesUtil.toBoolean(props.get(property), defaultValue);
            }
        } catch (IOException e) {
            LOGGER.error("Could not get property", e);
        }

        return defaultValue;
    }

    /**
     * Get the value of an OSGi configuration long property for a given PID.
     *
     * @param pid The PID of the OSGi component to retrieve
     * @param property The property of the config to retrieve
     * @param value The value to assign the provided property
     * @return The property value
     */
    public Long getProperty(final String pid, final String property, final Long defaultValue) {
        long placeholder = -1L;
        long defaultTemp = defaultValue != null ? defaultValue : placeholder;

        try {
            Configuration conf = configAdmin.getConfiguration(pid);

            @SuppressWarnings("unchecked")
            Dictionary<String, Object> props = conf.getProperties();

            if (props != null) {
                long result = PropertiesUtil.toLong(props.get(property), defaultTemp);

                return result == placeholder ? null : result;
            }
        } catch (IOException e) {
            LOGGER.error("Could not get property", e);
        }

        return defaultValue;
    }
}

OsgiConfigurationServiceConsumerExampleImpl.java

package com.nateyolles.sling.publick.services.impl;

import java.util.HashMap;
import java.util.Map;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;

import com.nateyolles.sling.publick.services.OsgiConfigurationService;

import org.osgi.framework.Constants;

/**
 * Sample service that uses the custom OSGiConfigurationService to
 * read and write OSGi configuration properties.
 */
@Service(value = OsgiConfigurationServiceConsumerExampleImpl.class)
@Component(name = "com.nateyolles.sling.publick.services.impl.OsgiConfigurationServiceConsumerExampleImpl", description = "example service which updates OSGi configs")
@Properties({
    @Property(name = OsgiConfigurationServiceConsumerExampleImpl.FOO_API_KEY, label = "Foo Key", description = "The Foo API key"),
    @Property(name = OsgiConfigurationServiceConsumerExampleImpl.FOO_ENABLED, boolValue = OsgiConfigurationServiceConsumerExampleImpl.ENABLED_DEFAULT_VALUE, label = "Enabled", description = "Enable Foo"),
    @Property(name = Constants.SERVICE_DESCRIPTION, value = "OsgiConfigurationServiceConsumerExample service"),
})
public class OsgiConfigurationServiceConsumerExampleImpl implements OsgiConfigurationServiceConsumerExample {

    /** Service to get and set OSGi properties. */
    @Reference
    private OsgiConfigurationService osgiService;

    /** PID of the current OSGi component */
    private static final String COMPONENT_PID = "com.nateyolles.sling.publick.services.impl.OsgiConfigurationServiceConsumerExample";

    /** OSGi property name for the API key */
    public static final String FOO_API_KEY = "foo.apiKey";

    /** OSGi property name for enabled */
    public static final String FOO_ENABLED = "foo.enabled";

    /** Default value for enabled */
    public static final boolean ENABLED_DEFAULT_VALUE = false;

    public void foo() {
        /* Get OSGi value for enabled property */
        boolean isEnabled = osgiService.getProperty(COMPONENT_PID, FOO_ENABLED, ENABLED_DEFAULT_VALUE);

        /* Set the OSGi value for enabled property */
        osgiService.setProperty(COMPONENT_PID, FOO_ENABLED, "true");

        /* Set multiple OSGi property values */
        Map<String, Object> properties = new HashMap<>();
        properties.put(FOO_ENABLED, true);
        properties.put(FOO_API_KEY, "foo");
        osgiService.setProperties(COMPONENT_PID, properties);
    }
}

Further reading:
Apache Felix Configuration Admin Service

Source: http://www.nateyolles.com/blog/2015/10/updating-osgi-configurations-in-aem-and-sling


By aem4beginner

1 comment:

  1. If I wanted to use this method to edit the publisher's config from the author instance, what else would I need to do? Any advice, tutorials, or code samples would be appreciated

    ReplyDelete

If you have any doubts or questions, please let us know.