Tuesday, March 16, 2010

CF: Dynamically changing Application.cfc using server specific configuration files

This started as I was reading an old post from Ben Nadel.
I did see that Ben was using a methodology to set up Application startup parameters in a special config function and commented on it.
I wrote about our experience with externalizing these parameters into specific configuration files instead and Ben stated that copying could be a problem if you override the config files.
Here then is my thoughts on how to keep external xml files to describe application specific settings, e.g. production vs test vs datasource names, timeouts, etc.
Nothing new here right? But the goal is to be able to keep the configuration files without a headache and conflicts.
Lets take a sample set of parameters in a file like so:

<wddxPacket version='1.0'>

<header/>

<data>

<struct>

<var name='REQUESTERRORHANDLER'>



<string>RequestErr.cfm</string>

</var>

<var name='VALIDATIONERRORHANDLER'>

<string>ValidationErr.cfm</string>



</var>

<var name='EnableErrorHandlers'>

<string>No</string>



</var>

<var name='DEBUG'>

<string>No</string>

</var>



<var name='ADMINEMAIL'>

<string>change_please@mycompany.com</string>

</var>

<var name='DATASOURCE'>



<string>APPDS</string>

</var>

<var name='APPLICATIONNAME'>

<string>StandardName</string>



</var>

</struct>

</data>

</wddxPacket>



The trick here is to be able to have a system which allows you to keep different configuration files without fear of overwriting them; and, even if you do copy over them by accident (or even if you have many different ones), the system will be smart enough to pick the correct one.

My implementation (Application.cfc) for this looks like this:



<cfcomponent>
<cfscript>
//start off by reading config file, read contents into this.stcConfig
initConfig();
//set application options from config file
this.Name=this.stcConfig.ApplicationName;
this.SessionManagement=
"Yes";
this.SessionTimeout=this.ConfigTimeout;
//other application parameters can be set
</cfscript>



<cffunction name="onApplicationStart" output="No">
<!--- no need to lock app scope variables in this function unless called from
another function --->


<!--- now make the config file data available to the application scope (bsoylu 03-17-2010) --->
<cfset Application.stcConfig = this.stcConfig>

<cfset Application.ConfigFile = this.ConfigFile>

<!--- we are setting some dynamic Error Handlers --->
<cfif IsDefined("this.stcConfig.EnableErrorHandlers") and this.stcconfig.EnableErrorHandlers>

<cferror type="REQUEST" template="#this.stcConfig.REQUESTERRORHANDLER#" mailto="#this.stcConfig.AdminEMail#">
<cferror type="VALIDATION" template="#this.stcConfig.VALIDATIONERRORHANDLER#" mailto="#this.stcConfig.AdminEMail#">
</cfif>

<!--- do other stuff now ...--->


</cffunction>


<cffunction name="OnRequestStart" >
<cfargument name="CallPage" type="string" hint="This is the page the user is calling. This argument is populated by CF automatically.">

<!--- do your request work --->

</cffunction>


<cffunction name="OnRequestEnd">
<!--- finish up your request code goes here --->

</cffunction>



<cffunction name="getIP" returntype="string" access="private" hint="return server main IP address">
<cfscript>
//get Inet Address type object from JVM

var objInetAddr = createObject("java", "java.net.InetAddress").getLocalHost();
return objInetAddr.getHostAddress();
</cfscript>
</cffunction>

<cffunction name="initConfig" access="private" output="Yes" hint="load config file">

<cftry>
<!--- first check for server specific config file config.XXX.XXX.XXX.XXX.xml, e.g. config.196.18.12.78.xml --->
<cfset strConfigFile = getDirectoryFromPath(getCurrentTemplatePath()) & "config.#getIP()#.xml">

<cfif not FileExists(strConfigFile)>
<!--- fall back to global config file --->
<cfset strConfigFile = getDirectoryFromPath(getCurrentTemplatePath()) & "config.xml">
</cfif>

<cfif FileExists(strConfigFile)>

<cffile action="READ" file="#strConfigFile#" variable="strConfigWDDX">
<cfif iswddx(strConfigWDDX)>

<cfwddx action="WDDX2CFML" input="#Trim(strConfigWDDX)#" output="this.stcConfig">
<!--- save the config file that we use --->
<cfset this.ConfigFile = strConfigFile>
<cfelse>

<cfthrow type="INIT" detail="Incorrect Configuration File. No WDDX detected.">
</cfif>
<cfelse>
<cfthrow type="INIT" detail="Missing Configuration File. No global or server [#getIP()#] specific configuration file found.">

</cfif>

<!--- prepare timeouts --->
<cfif IsDefined("this.stcConfig.SessionTimeOutMinutes") and Val(this.stcConfig.SessionTimeOutMinutes) GT 0>

<cfset this.ConfigTimeout = CreateTimeSpan(0,0,Val(this.stcConfig.SessionTimeOutMinutes),0)>

<cfelse>
<cfset this.ConfigTimeout = CreateTimeSpan(0,0,20,0)>

</cfif>

<cfcatch type="Any">
<cfoutput>#cfcatch.detail#</cfoutput>
<cfabort>
</cfcatch>
</cftry>
</cffunction>

</cfcomponent>


The idea here that the Application.cfc selectivly loads configuration files. It looks for a server specific configuration file, e.g. config.10.10.1.100.xml , then a global configuration file, e.g. config.xml.
Allmost all of the important work occurs in the initConfig() function. It determines the machine's IP address then alternatly looks for a machine specific configuration file, followed by a global configuration file. If neither can be found, we throw an error.

Thus, even if I copied my development server's configuration file by accident to the production server it would not be of consequence for the production server. I am of course subject to the normal human errors. If I delete all configuration files I am hosed anyway etc.

Try it out and let me know if this helps.

Cheers,
-Bilal