Dynamic Configuration with Environment Config And a Coldspring Config Factory
2008 June 16
When developing web application, it is very likely that at some point you will need to create dynamic configuration for your application in terms of development, staging and production environments. I recently discovered the excellent Environment Config project by Rolando Lopez. Rolando has created a very interesting package with lots of robust features for configuring your application dynamically. There are portions of the package that I must admit, I'm not using (yet). What follows is my current setup for integrating the environment config with a custom factory.
Getting Started
To use the environment config, you create an xml file in this format.<?xml version="1.0" encoding="UTF-8"?>
<environments>
<default>
<config>
<!-- main vars -->
<property name="adminEmail">admin@domain.com</property>
<property name="transferConfigPath">/config/transfer/Transfer.xml</property>
<property name="definitionsPath">/config/definitions</property>
<property name="exampleMap">
<map>
<entry key="key1"><value>value1</value></entry>
<entry key="key2"><value>value2</value></entry>
</map>
</property>
</config>
</default>
<!-- local -->
<environment id="local">
<patterns>
<pattern>^project.fancybread.local</pattern>
</patterns>
<config>
<property name="mailServer">localhost</property>
<property name="datasourcePath">/config/transfer/local_ds.xml</property>
</config>
</environment>
<!-- staging -->
<environment id="staging">
<patterns>
<pattern>^project.fancybread.local</pattern>
</patterns>
<config>
<property name="mailServer">staging.mailserver</property>
<property name="datasourcePath">/config/transfer/staging_ds.xml</property>
</config>
</environment>
<!-- production -->
<environment id="production">
<patterns>
<pattern>^www.somedomain.com</pattern>
</patterns>
<config>
<property name="mailServer">production.mailserver</property>
<property name="datasourcePath">/config/transfer/production_ds.xml</property>
</config>
</environment>
</environments>
I've trimmed down the settings for this example. The important thing to note is that the Environment Config will return a struct of the properties you define in the default config and the specific environment config properties by calling either getEnvironmentById() or getEnvironmentByUrl() passing either the environment id or the host name that is matched by regular expression to the pattern of you environment. I like to use the host name and getEnvironmentByUrl().
Coldspring Integration
To configure Environment for use in Coldspring, I set a bean definition for it and a custom config factory named (most unimaginatively) ConfigFactory. Here are the Coldspring bean definitions.<bean id="environmentConfig" class="model.Environment">
<constructor-arg name="xmlFile">
<value>${configFile}</value>
</constructor-arg>
</bean>
<bean id="configFactory" class="model.ConfigFactory">
<constructor-arg name="hostName">
<value>${hostName}</value>
</constructor-arg>
<constructor-arg name="environmentConfig">
<ref bean="environmentConfig" />
</constructor-arg>
</bean>
Here's the Application.cfc code for setting up Coldspring. Rather than pass a slough of variables to Coldspring, I'm passing only the location for the environment config xml and the CGI.HTTP_HOST.
<cfset var beanDefFileLocation = expandPath('/config/beans.xml.cfm')>
<cfset var params = structNew() />
<cfset params['hostName'] = CGI.HTTP_HOST>
<cfset params['configFile'] = '/config/environment.xml.cfm'>
<cfset application.beanFactory = createObject("component","coldspring.beans.DefaultXmlBeanFactory").init(StructNew(),params) />
<cfset application.beanFactory.loadBeans(beanDefinitionFileName=beanDefFileLocation, constructNonLazyBeans=false) />
Coldspring Factory Beans At Work
When I started learning Coldspring, I had no idea how to use it beyond the most basic configurations. One item that puzzled me were factory beans. I incorrectly assumed that a factory bean required some special magic and a bean declaration in the condspring xml had to bean object. It wasn't until recently that I learned that this is not set in stone. A "bean" declaration in Coldspring can be any value and a factory bean and factory method can be defined for any object. Armed with that knowledge, I set out to wrap the environment config in a simple "factory" to easily swap values based on the HTTP_HOST. Here's the code for setting the Transfer config using the ConfigFactory.<bean id="transferFactory" class="transfer.TransferFactory">
<constructor-arg name="datasourcePath"><ref bean="datasourcePath" /></constructor-arg>
<constructor-arg name="configPath"><ref bean="transferConfigPath" /></constructor-arg>
<constructor-arg name="definitionPath"><ref bean="definitionsPath" /></constructor-arg>
</bean>
<bean id="datasource" factory-bean="transferFactory" factory-method="getDatasource" />
<bean id="transfer" factory-bean="transferFactory" factory-method="getTransfer" />
<bean id="transferConfigPath" factory-bean="configFactory" factory-method="getSetting">
<constructor-arg name="key">
<value>transferConfigPath</value>
</constructor-arg>
</bean>
<bean id="datasourcePath" factory-bean="configFactory" factory-method="getSetting">
<constructor-arg name="key">
<value>datasourcePath</value>
</constructor-arg>
</bean>
<bean id="definitionsPath" factory-bean="configFactory" factory-method="getSetting">
<constructor-arg name="key">
<value>definitionsPath</value>
</constructor-arg>
</bean>
While passing arguments to the getSetting() as a factory method works, I found this to be rather verbose, so I used it as an opportunity to incorporate onMissingMethod in an cfc for the first time.
Simplifying the Config with OnMissingMethod
OnMissingMethod is a very powerful feature of Coldfusion 8. I've seen some pretty fantastic examples of software that leverage onMissingMethod (oMM). By using a very simple convention for my oMM in my ConfigFactory, I'm able to reduce all the definitions above that use getSetting() to the following.<bean id="transferConfigPath" factory-bean="configFactory" factory-method="getTransferConfigPath" />
<bean id="datasourcePath" factory-bean="configFactory" factory-method="getDatasourcePath" />
<bean id="definitionsPath" factory-bean="configFactory" factory-method="getDefinitionsPath" />
Here's the full code for the ConfigFactory.
<cfcomponent displayname="ConfigFactory" output="false">
<cfset variables.settings = structNew()>
<cffunction name="init" access="public" output="false" returntype="any">
<cfargument name="hostName" type="string" required="true">
<cfargument name="environmentConfig" type="model.Environment" required="true">
<cfset variables.settings = arguments.environmentConfig.getEnvironmentByUrl(arguments.hostName)>
<cfreturn this>
</cffunction>
<cffunction name="getAllSettings" access="public" output="false" returntype="struct">
<cfreturn variables.settings>
</cffunction>
<cffunction name="getSetting" access="public" output="false" returntype="any">
<cfargument name="key" type="string" required="true">
<cftry>
<cfreturn variables.settings[arguments.key]>
<cfcatch type="any">
<cfthrow type="custom" message="Setting does not exist.">
</cfcatch>
</cftry>
</cffunction>
<cffunction name="onMissingMethod" access="public" output="false" returntype="any">
<cfargument name="MissingMethodName" type="string" required="true" />
<cfargument name="MissingMethodArguments" type="struct" required="true" />
<cfif (Left(arguments.MissingMethodName,3) eq "get")>
<cfreturn getSetting(Right(arguments.MissingMethodName,Len(arguments.MissingMethodName)-3))>
</cfif>
</cffunction>
</cfcomponent>
Using onMissingMethod in this way makes it a breeze to add properties to my environment configuration and later reference them using the ConfigFactory and a correctly named factory-method.
Granted, there are many different ways to dynamically create configuration settings, using ant for instance. I'm not yet an ant expert. Until then, I like the combination of the environment config and the dynamic config factory.