DX: Module configuration using property files

Jahia est la seule Digitale eXperience Plateforme qui vous permet réellement de proposer des parcours personnalisés optimisés par les données clients. En savoir plus

In the interest of making the lives easier whether it be for the System Admin Teams that maintain/deploy a Jahia project or testers and developers that have to test the application based on different values, we often find that it is easier to have our code use some sort of configuration information to drive its behavior. Quite often this is achieved by using property files to provide the configuration information.

In this Blog we will cover how to use property files that are loaded by Spring framework that Jahia DX uses and also how to branch out to use property files that are not in the default folders structures.

The Use Case

I have a requirement where I need to present my authors with a list of active hotels (in a choicelist). To get this list, I need to call an external service that also requires a username and password.

Things to consider

  1. Will the username and password change?
  2. Will the endpoint for the service change as code moves from environments, i.e. dev to qa to staging and finally to production?
  3. Will the properties be different from environment to environment?

Assuming the answers to these questions is “Yes” then a hardcode endpoint, username and password in the code would require recompilation and redpoloyment any time there is a change to any of these attributes, which would make it highly impractical.

Solution

The solution is to have each of these attributes - endpoint, username and password - be configurable via a property file. Additionally, the property file that will be used will be utilized will be dependent upon the environment (dev, qa, staging, prod etc).

Below is an excerpt of properties-example.xml which is used as the spring definition for my module.

<bean id="modulePlaceholderConfig"
     class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
   <property name="properties" ref="jahiaProperties"/>
   <property name="locations">
       <list>
           <value>classpath*:jahia-modules-*.properties</value>
           <value>classpath*:jahia/jahia-modules-*.properties</value>
           <value>osgibundle:META-INF/config/${df.env:dev}/org.jahia.modules.configuration.service.properties</value>
       </list>
   </property>
   <property name="ignoreResourceNotFound" value="true"/>
   <property name="localOverride" value="true"/>
</bean>


<bean id="demoPropertiesBean" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
   <property name="locations">
       <list>
           <value>file:///${ext.config.path}demo${df.env:}.properties</value>
       </list>
   </property>
   <property name="ignoreResourceNotFound" value="true"/>
</bean>


<bean id="demoInitializer" class="org.jahia.initializers.DemoInitializer" >
   <property name="key" value="demoInitializer"/>
   <property name="demoProperties" ref="demoPropertiesBean"/>
   <property name="serviceUrl" value="${service.url:noUrl}"/>
   <property name="serviceUser" value="${service.username:noUser}"/>
   <property name="servicePassword" value="${service.password:noPass}"/>
</bean>

XML Explained

modulePlaceHolderConfig bean

<bean id="modulePlaceholderConfig"
     class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
   <property name="properties" ref="jahiaProperties"/>
   <property name="locations">
       <list>
           <value>classpath*:jahia-modules-*.properties</value>
           <value>classpath*:jahia/jahia-modules-*.properties</value>
           <value>osgibundle:META-INF/config/${df.env:dev}/org.jahia.modules.configuration.service.properties</value>
       </list>
   </property>
   <property name="ignoreResourceNotFound" value="true"/>
   <property name="localOverride" value="true"/>
</bean>
  1. Bean is present to provide an override of the properties files loaded by this module. Otherwise only the property files loaded will be those described in this article:
    jahia-modules-*.properties
    
    jahia/jahia-modules-*.properties
    Without this, it is not currently possible to load property files from a different location.

  2. The below line is where we load a properties file that does located in either jahia-modules-*properties or jahia/jahia-modules-*.properties
    <value>osgibundle:META-INF/config/${df.env:dev}/org.jahia.modules.configuration.service.properties</value>
    osgibundle - the starting point of this is “resources” folder in your module’s codebase ${df.env:dev} –  df.env refers to a property that may have been loaded to define the environment otherwise dev is used as the default value.

    You can set the value as a java run time parameter by using –Ddf.env-=qa or even a property in the jahia.properties file by adding an entry df.env=qa.

    By having access to a property which defines the environment, the loading of the properties file can be environment aware.

    Later in the XML you will see that there are 3 properties server.url, server.username and server.password that are used. These 3 properties are present in the org.jahia.modules.configuration.service.properties file that was loaded.

demoPropertiesBean bean

<bean id="demoPropertiesBean" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
   <property name="locations">
       <list>
           <value>file:///${ext.config.path}demo${df.env:}.properties</value>
       </list>
   </property>
   <property name="ignoreResourceNotFound" value="true"/>
</bean>
  1. The purpose of defining this bean is to create a properties bean contains all the properties within a property file rather than referencing each property one by one. With this approach, if desired, the class can be made into a service which then can expose the properties to other classes/modules within the Jahia platform and thus centralizing the loading property files to a single module.

  2. With the value below you will see now that I am loading the properties file from the rather than how we had used the osgibundle notation previously which meant that the properties file was packaged within the module. This can be useful if you want to externalize the properties file from your module such that they can be modified without having to redeploy the whole module. However, as this bean is initialized just once, the module would have to get stopped/started for it to reload the properties.
    file:///${ext.config.path}demo${df.env:}.properties</value>
    NOTE : it is assumed (and important) that ext.config.path is a property that has been set either using the JVM –D option or in one of the property files loaded prior, such as the jahia.properties. If this is not done then there will be an error. The reason for using ext.config.path in this example is to not have a fixed path and providing the system owners flexibility on where they store the properties file rather than having it hard coded in the Spring XML. In this way one can reduce having to redeploy the module to simply make a path change.

    Additionally you will see the ${df.env:} – after the colon there is nothing, so the default value will be nothing. As such if the df.env property is not set, then the property file that will be loaded will be demo.properties under the path specified.

demoInitializer bean

<bean id="demoInitializer" class="org.jahia.initializers.DemoInitializer" >
   <property name="key" value="demoInitializer"/>
   <property name="demoProperties" ref="demoPropertiesBean"/>
   <property name="serviceUrl" value="${service.url:noUrl}"/>
   <property name="serviceUser" value="${service.username:noUser}"/>
   <property name="servicePassword" value="${service.password:noPass}"/>
</bean>
  1. The purpose of defining this bean to create a simple choicelist initializer. Per the use case described, this choicelist initializer would then use the server.url, server.username and server.password in calling a service and then provide a user of Active hotels.

    The property demoProperties is the bean that we defined in the last section and it is injected into the Initializer. In this way it can have access to all the properties if required.

    The properties serviceUrl, serviceUser, servicePassword – this just shows example of injecting a single property into Initializer and passing a default value if the property isn’t present.

DemoInitializer.java Explained

package org.jahia.initializers;


import org.apache.jackrabbit.value.StringValue;

import org.jahia.services.content.JCRNodeWrapper;

import org.jahia.services.content.JCRSessionFactory;

import org.jahia.services.content.decorator.JCRSiteNode;

import org.jahia.services.content.nodetypes.ExtendedPropertyDefinition;

import org.jahia.services.content.nodetypes.initializers.ChoiceListValue;

import org.jahia.services.content.nodetypes.initializers.ModuleChoiceListInitializer;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;


import javax.jcr.NodeIterator;

import javax.jcr.RepositoryException;

import javax.jcr.query.Query;

import javax.jcr.query.QueryManager;

import javax.jcr.query.QueryResult;

import java.util.*;


/**

* Created by nikhilpatel on 12/8/15.

*/

public class DemoInitializer implements ModuleChoiceListInitializer {

   private transient static Logger logger = LoggerFactory.getLogger(DemoInitializer.class);

   private String key;


   private Properties demoProperties;


   private String serviceUrl;

   private String serviceUser;

   private String servicePassword;


   private String url;


   private String clientId;


   public List<ChoiceListValue> getChoiceListValues(ExtendedPropertyDefinition epd, String param,

                                                    List<ChoiceListValue> values, Locale locale,

                                                    Map<String, Object> context) {


       if (demoProperties == null) logger.error("demoProperties is null - something went wrong");


       logger.info("clientId is : " + demoProperties.getProperty("clientId"));

       List<ChoiceListValue> choiceListValues = new ArrayList<ChoiceListValue>();

       /* Here is where we would use the properties to call a service  */

       /* clientId retrieved from demoProperties because the service requires a special property */

       /* serverUrl, serverUser and serverPassword for the relevant properties for the endpoint */

       choiceListValues.add(new ChoiceListValue("value1", null, new StringValue("1")));

       choiceListValues.add(new ChoiceListValue("value2", null, new StringValue("2")));

       choiceListValues.add(new ChoiceListValue("value3", null, new StringValue("3")));

       return choiceListValues;

   }


   public void setKey(String key) {

       this.key = key;

   }


   public String getKey() {

       return key;

   }


   public void setUrl(String url) {

       logger.info("in setUrl - url is : " + url);

       this.url = url;

   }


   public String getUrl(){

       return url;

   }


   public void setClientId(String clientId) {

       logger.info("**********************  in setClientId - clientId is : " + clientId);

       this.clientId = clientId;

   }


   public String getClientId(){

       return clientId;

   }


   public void setDemoProperties(Properties demoProperties) {


       this.demoProperties = demoProperties;

       if (demoProperties == null) logger.info("DEMO PROP NULL");

       logger.info("clientId is : " + demoProperties.getProperty("clientId"));

       logger.info("oauth.url is : " + demoProperties.getProperty("oauth.url"));

   }


   public Properties getDemoProperties(){

       return demoProperties;

   }


   public String getServiceUrl() {

       return serviceUrl;

   }


   public void setServiceUrl(String serviceUrl) {

       logger.info("serviceUrl is : " + serviceUrl);

       this.serviceUrl = serviceUrl;

   }


   public String getServiceUser() {

       return serviceUser;

   }


   public void setServiceUser(String serviceUser) {

       logger.info("serviceUser is : " + serviceUser);

       this.serviceUser = serviceUser;

   }


   public String getServicePassword() {

       return servicePassword;

   }


   public void setServicePassword(String servicePassword) {

       logger.info("servicePassword is : " + servicePassword);

       this.servicePassword = servicePassword;

   }

}

In the getChoiceListValues method where the choicelist is built, I can make use of either the properties set directly by the different property setters in demoInitializer bean or can make use of the demoProperties which contains all properties that were retrieved in the demoPropertiesBean configuration.

The rest of the methods in the class are simple getters and setters. The setters specifically are needed so the initialization can happen in the spring xml. The setDemoProperties is one of the setter methods utilized by the spring definition.

Summary

Using the Spring framework it is possible to load property files in a Jahia module and then utilize those property files to drive the behavior of an application. It is also possible to drive the loading of the property files based on environment variables or other properties so as to make maintenance and management easier.

Using property files is only one way within Jahia DX for driving your application’s behavior via configuration. Other options are :

  1. OSGi Configuration. For more information on this please read the Blog : Working with Properties in DX
  2. Through Blueprints. For an example for utilizing this approach, please read.

Code Example

The example code can be found on Github https://github.com/Jahia/properties-example. If you can think of any improvements or have any questions, please email me nikpatel@jahia.com.

Nikhil Patel
Retour