Sunday, June 03, 2007

Extending Spring PropertyPlaceholderConfigurer to consider the OS Platform

It is very common with Java to develop on Windows but deploy on UNIX. One of the main pains when switching between a Windows platform and a UNIX platform is the different ways they represent directory paths. e.g. the Windows path:

H:\users\robertmaldon

might have a UNIX equivalent that looks something like this:

/home/robertmaldon

The JVM actually does a pretty good job of coping with these path differences. For example, if you specify a path like "C:/software/hype" and the JVM knows it is running on Windows then it will internally convert the path to "C:\software\hype". It can even cope with a path containing mixed forward slashes and back slashes. e.g. "C:\software\hype/config/config.properties"

If you are Spring in your application then a common way to configure your application is to use the PropertyPlaceholderConfigurer class. This class replaces values in a Spring configuration file with property values from a properties file and/or system properties.

How do you manage the different paths when devloping for both Windows and UNIX? You could maintain two sets of property files - one for Windows devlopment and one for UNIX development - but this gets unwieldy when you have to define a lot of interconnected properties.

Instead I propose an alternative, a small extension to PropertyPlaceholderConfigurer that works like this:
  1. If you specify a property replacement like ${somevar} it will first look for a property named PLATFORM.somevar
  2. If it cannot find a property named PLATFORM.somevar then it will default to the old behaviour. i.e. look for a property named somevar
The value of PLATFORM is derived from the Java system property os.name. On Windows this property has the value of "Windows XP" or "Windows Me", etc. On Linux it has the value of "Linux". On Solaris it has the value of "SunOS". And so on. Using the actual value of os.name can be a bit ugly (particularly when spaces are involved) so my extension maps os.name to something more friendly. e.g. on Windows PLATFORM is mapped to "win"; on Solaris it is mapped to "sun".

So what does all of this look like together?

Let's say you define a simple POJO like this:


package robertmaldon.config;

public class PlatformHolder {
private String value;

public void setValue(String value) {
this.value = value;
}

public String toString() {
return "value=[" + value + "]";
}
}


Your Spring applicationContext.xml looks something like this:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean class="robertmaldon.config.PlatformPropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath:application.properties</value>
</property>
</bean>

<bean id="platformHolder" class="robertmaldon.config.PlatformHolder">
<property name="value" value="${somepath}"/>
</bean>
</beans>


Your application.properties file looks like this:


somehome=/home/robertmaldon
win.somehome=H:/users/robertmaldon
somepath=${somehome}/config


And finally a main class like this:


package robertmaldon.config;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class PlatformMain {
public static void main(String[] args) throws InterruptedException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

PlatformHolder holder = (PlatformHolder)context.getBean("platformHolder");
System.out.println(holder.toString());
}
}


If running this progam on Windows the output of this progam would be:

value=[H:/users/robertmaldon/config]

and on any other platform the output would be:

value=[/home/robertmaldon/config]

So after all of that leadup here is what the PlatformPropertyPlaceholderConfigurer extension class looks like:


package robertmaldon.config;

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

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;

/**
* <p>A small extension to org.springframework.beans.factory.config.PlatformPropertyPlaceholderConfigurer
* that, when asked to replace the placeholder "somevar", will first look for
* a property named "platformprefix.somevar", or failing a match will then look for
* the property "somevar".</p>
*
* <p>The "platformprefix" part of the placholder is derived from the Java system
* property "os.name". For convenience the "os.name" value is mapped to a
* prefix that is easier to type. For example, on Windows XP the value of "os.name"
* is "Windows XP" and this class maps "platformprefix" to "win".</p>
*
* <p>This class has a default set of mappings (see DEFAULT_PLATFORM_PREFIX_MAPPINGS)
* which can be overridden by setting the property platformPrefixMappings.</p>
*
* <p>See http://lopica.sourceforge.net/os.html for an extensive list of
* platform names used by the os.name Java system property.</p>
*/
public class PlatformPropertyPlaceholderConfigurer
extends PropertyPlaceholderConfigurer implements InitializingBean {

private static final Map DEFAULT_PLATFORM_PREFIX_MAPPINGS;

static {
DEFAULT_PLATFORM_PREFIX_MAPPINGS = new HashMap();
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("AIX", "aix");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Digital Unix", "dec");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("FreeBSD", "bsd");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("HP-UX", "hp");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Irix", "irix");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Linux", "linux");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Mac OS", "mac");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Mac OS X", "mac");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("MPE/iX", "mpe");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Netware 4.11", "netware");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("OpenVMS", "vms");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("OS/2", "os2");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("OS/390", "os390");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("OSF1", "osf1");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Solaris", "sun");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("SunOS", "sun");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows 2000", "win");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows 2003", "win");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows 95", "win");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows 98", "win");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows CE", "win");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows Me", "win");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows NT", "win");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows NT (unknown)", "win");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows Vista", "win");
DEFAULT_PLATFORM_PREFIX_MAPPINGS.put("Windows XP", "win");
}

private Map platformPrefixMappings;

private String platformPrefix;

/**
* Override the default platform prefix mappings.
*
* @param platformPrefixMappings
*/
public void setPlatformPrefixMappings(Map platformPrefixMappings) {
this.platformPrefixMappings = platformPrefixMappings;
}

/**
* Attempt to determine the prefix to use for this platform.
* First check any user defined prefix mappings. If no match
* then check the default platform mappings.
*/
public void afterPropertiesSet() throws Exception {
String platform = System.getProperty("os.name");
if (platformPrefixMappings != null) {
platformPrefix = platformPrefixMappings.get(platform);
}
if (platformPrefix == null) {
platformPrefix = DEFAULT_PLATFORM_PREFIX_MAPPINGS.get(platform);
}
}

/**
* Override the PropertyPlaceholderConfigurer.resolvePlaceholder(...) method
* to first look for a placeholder with the platform prefix.
*/
protected String resolvePlaceholder(String placeholder, Properties props, int systemPropertiesMode) {
String propVal = null;
if (systemPropertiesMode == SYSTEM_PROPERTIES_MODE_OVERRIDE) {
if (platformPrefix != null) {
propVal = resolveSystemProperty(platformPrefix + "." + placeholder);
}
if (propVal == null) {
propVal = resolveSystemProperty(placeholder);
}
}
if (propVal == null) {
if (platformPrefix != null) {
propVal = resolvePlaceholder(platformPrefix + "." + placeholder, props);
}
if (propVal == null) {
propVal = resolvePlaceholder(placeholder, props);
}
}
if (propVal == null && systemPropertiesMode == SYSTEM_PROPERTIES_MODE_FALLBACK) {
if (platformPrefix != null) {
propVal = resolveSystemProperty(platformPrefix + "." + placeholder);
}
if (propVal == null) {
propVal = resolveSystemProperty(placeholder);
}
}
return propVal;
}
}

5 comments:

Anonymous said...

Great article.
why do you have to implement initializeBean interface?

Robert Maldon said...

The InitializeBean interface is implemented in order to give you the opportunity to override the default platform prefix mappings.

Since this is a custom class you would probably just change the values inside the static block instead of invoking setPlatformPrefixMappings(Map), but I added the ability to override the defaults since that is what core Spring classes usually allow you to do.

Anonymous said...

Very useful article! Thanks a lot!!!

lango said...

thanks a lot! nice solution!

Anonymous said...

Very well written and explained. Thanks a lot