Custom App XML Configuration File

Nearly every site contains a URL, business name, common email addresses (sales, info, webmaster, etc.), address and contact phone numbers, right? What happens if you build an application and one of those things changes? My first inclination was to store this information in a database, pull it when the application initialize and cache it. This worked great... but what if there was a problem connecting to the database? It also seemed logical to keep all of my configuration in a common area, rather than spread it between XML files for my frameworks and the database. The result was an AppConfig.xml.cfm and AppConfigService.cfc.

AppConfig.xml.cfm


This file is pretty straight forward. You'll see I have several application "elements" with settings assigned to each. I also created an XSD to validate the XML file against to catch any fat fingering.

<?xml version="1.0" encoding="utf-8"?>
<app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:noNamespaceSchemaLocation="config_0.2.xsd">


   <!-- General -->
   <company>
      <setting name="Name" value="My Business" />
      <setting name="Abbr" value="MBusiness" />
      <setting name="Domain" value="my-business.com" />
      <setting name="Phone" value="(555) 555-1234" />
      <setting name="TollFree" value="(888) 555-1234" />
      <setting name="Fax" value="(555) 555-4321" />
   </company>   
   
   <!-- Database -->
   <db>
      <setting name="DSN" value="myDSN" />
      <setting name="User" value="myUser" />
      <setting name="Password" value="myPassword" />
      
      <appHost name="server01.my-business.com">
         <setting name="DSN" value="prodDSN" />
         <setting name="User" value="prodUser" />
         <setting name="Password" value="prodPassword" />
      </appHost>
   </db>
   
   <!-- Mail server -->
   <mail>
      <setting name="MailServer" value="mail.my-business.com" />
      <setting name="MailUser" value="mailUser" />
      <setting name="MailPassword" value="mailPassword" />
      <setting name="RequiresAuthentication" value="true" />

      <appHost name="server01.my-business.com">
         <setting name="MailServer" value="prod.my-business.com" />
         <setting name="MailUser" value="" />
         <setting name="MailPassword" value="" />
         <setting name="RequiresAuthentication" value="false" />
      </appHost>
   </mail>
   
   <!-- Frequently used email accounts -->
   <mailAccounts>
      <setting name="Admin" value="admin@my-business.com" />
      <setting name="Sales" value="sales@my-business.com" />
      <setting name="Support" value="support@my-business.com" />
      <setting name="Service" value="service@my-business.com" />
      <setting name="General" value="info@my-business.com" />
   </mailAccounts>
   
   <!-- Security -->   
   <security>
      <setting name="PasswordExpiresIn" value="90" /> <!-- User password expiration, in days -->
      <setting name="ReportPassword" value="SekritPassword" /> <!-- General report password, PDF only -->      
   </security>
   
   <!-- DO NOT CHANGE WITHOUT UPDATING USER PASSWORDS IN DATABASE -->
   <!-- Encryption -->
   <encryption>
      <setting name="Algorithm" value="AES" />
      <setting name="Encoding" value="HEX" />
      <setting name="Key" value="thisIsSekrit" />
   </encryption>
</app>

If you have read any of my other blog posts, you'll find that I'm a fan of ColdBox. And if you have any experience with ColdBox, you know that there is a spot in the ColdBox XML configuration file called "YourSettings" which also could have served the same purpose. However, I like the portability of this approach should a project not require a framework, or require a different framework.

The next task was to parse this XML file into something ColdFusion could use, and made sense to the developer (me!).

AppConfigService.cfc

<cfcomponent displayname="AppConfigService" hint="This is the AppConfigService component" output="false">
   
   <cffunction name="init" access="public" output="false" returntype="model.AppConfigService">
      <cfargument name="appConfigPath" hint="The path to the application configuration xml file. Should be a relative path, i.e. /config/appConfig.xml.cfm" type="string" required="true" />
      <cfscript>
         configure(arguments.appConfigPath);
         return this;
      </cfscript>
   </cffunction>
   
   <!--- PUBLIC --->

   <!--- BEGIN || Parse config --->
   <cffunction name="configure" access="public" returntype="any" output="false">
      <cfargument name="appConfigPath" hint="The path to the application configuration xml file. Should be a relative path, i.e. /config/appConfig.xml.cfm" type="string" required="true" />
      <cfscript>
         var a = 0;         
         var c = 0;
         var s = 0;
         var i = 0;
         var appLen = "";
         var configFile = "";
         var configXml = "";
         var xPath = "/app/";
         var sectionArray = arrayNew(1);
         var hostName = getFQDN();
         variables.app = structNew();
         
         //Read configuration file
         configFile = readFile(expandPath(arguments.appConfigPath));
         
         //Parse config file
         configXml = xmlParse(trim(configFile),false);
         
         //Store number of configuration sections
         appLen = arrayLen(configXML.app.xmlChildren);
         
         //Create array of sections
         for (a=1; a LTE appLen; a++) {
            arrayAppend(sectionArray,configXML.app.xmlChildren[a]);
         }
         
         //Extract variables
         //Loop over sections, create new structure for each. Store in var'd app struct
         for (c=1; c LTE appLen; c++) {
            app[sectionArray[c].xmlName] = structNew();
            
            //If the node is a "setting", set it
            if(structkeyExists(sectionArray[c],"setting")) {   
               
               //Loop over settings and add to app struct
               for (s=1; s LTE arrayLen(sectionArray[c].setting); s++) {
                  settingName = sectionArray[c].setting[s].xmlAttributes["name"];
                  settingValue = sectionArray[c].setting[s].xmlAttributes["value"];
                  app[sectionArray[c].xmlName][settingName] = settingValue;
               }
            }

            //If the node is a "appHost", set environmental specific variables            
            if(structKeyExists(sectionArray[c],"appHost")) {
               
               //Loop over "appHost" elements
               for (s=1; s LTE arrayLen(sectionArray[c].appHost); s++) {
                  
                  //Check if host element contains this server's FQDN
                  if(listFindNoCase(sectionArray[c].appHost[s].xmlAttributes["name"], hostName)) {
                     
                     //Loop over settings and add to app struct
                     for (i=1; i LTE arrayLen(sectionArray[c].appHost[s].setting); i++) {
                        settingName = sectionArray[c].appHost[s].setting[i].xmlAttributes["name"];
                        settingValue = sectionArray[c].appHost[s].setting[i].xmlAttributes["value"];
                        app[sectionArray[c].xmlName][settingName] = settingValue;
                     }
                  }
               }
            }   
         }
         
         //Add hostname to app for future use
         structInsert(app,"hostName",hostName);         
      </cfscript>
   </cffunction>
   <!--- END || Parse config --->
   
   <!--- BEGIN || GETTERS & SETTERS --->         
   <!--- BEGIN || Site variables --->
   <cffunction name="getSite" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfreturn variables.app.company[arguments.myVar] />   
   </cffunction>
   <cffunction name="setSite" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfargument name="myValue" type="string" required="true">
      <cfset variables.app.company[arguments.myVar] = arguments.myValue>
   </cffunction>
   <!--- END || Site variables --->   
   
   <!--- BEGIN || DB settings --->
   <cffunction name="getDB" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfreturn variables.app.db[arguments.myVar] />
   </cffunction>
   <cffunction name="setDB" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfargument name="myValue" type="string" required="true">
      <cfset variables.app.db[arguments.myVar] = arguments.myValue>
   </cffunction>
   <!--- END || DB settings--->
   
   <!--- BEGIN || Encryption settings --->
   <cffunction name="getEncryption" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfreturn variables.app.encryption[arguments.myVar] />
   </cffunction>
   <cffunction name="setEncryption" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfargument name="myValue" type="string" required="true">
      <cfset variables.app.encryption[arguments.myVar] = arguments.myValue>
   </cffunction>
   <!--- END || Encryption settings--->   
   
   <!--- BEGIN || Mail settings --->
   <cffunction name="getMail" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfreturn variables.app.mail[arguments.myVar] />
   </cffunction>
   <cffunction name="setMail" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfargument name="myValue" type="string" required="true">
      <cfset variables.app.mail[arguments.myVar] = arguments.myValue>
   </cffunction>
   <!--- END || Mail settings--->
   
   <!--- BEGIN || Mail accounts --->
   <cffunction name="getMailAccount" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfreturn variables.app.mailAccounts[arguments.myVar] />
   </cffunction>
   <cffunction name="setMailAccount" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfargument name="myValue" type="string" required="true">
      <cfset variables.app.mailAccounts[arguments.myVar] = arguments.myValue>
   </cffunction>
   <!--- END || Mail accounts --->
   
   <!--- BEGIN || Security --->
   <cffunction name="getSecurity" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfreturn variables.app.security[arguments.myVar] />
   </cffunction>
   <cffunction name="setSecurity" access="public" output="false" returnType="string">
      <cfargument name="myVar" type="string" required="true">
      <cfargument name="myValue" type="string" required="true">
      <cfset variables.app.secruity[arguments.myVar] = arguments.myValue>
   </cffunction>
   <!--- END || Security --->

   <!--- BEGIN || Host name --->
   <cffunction name="getHostName" access="public" output="false" returnType="string">
      <cfreturn variables.app.hostName />
   </cffunction>
   <cffunction name="setHostName" access="public" output="false" returnType="string">
      <cfargument name="myValue" type="string" required="true">
      <cfset variables.app.hostName = arguments.myValue>
   </cffunction>
   <!--- END || Host name --->   
   <!--- END || GETTERS & SETTERS --->         

   <!--- PRIVATE --->
   <!--- BEGIN || Read file--->
   <cffunction name="readFile" access="private" output="false" returntype="string" hint="Facade to Read a file's content">
      <cfargument name="FileToRead" type="String" required="yes" hint="The absolute path to the file.">

      <cfset var FileContents = "">
      <cffile action="read" file="#arguments.FileToRead#" variable="FileContents">
      <cfreturn FileContents>
   </cffunction>
   <!--- END || Read file--->
   
   <!--- BEGIN || Get this server name --->
   <cffunction name="getFQDN" access="private" returntype="string" hint="Retreives FQDN from host" output="false" >
      <cfscript>
         var InetAddress = createObject("java", "java.net.InetAddress");
         var hostName = InetAddress.getLocalHost().getHostName();
         
         return hostName;
      </cfscript>
   </cffunction>   
   <!--- END || Get this server name --->

</cfcomponent>

There isn't whole lot of magic hidden in here. The init method calls configure(), which reads and parses the XML file into an "app" structure. Then I have a series of getters/setters to retrieve the settings I need.

How to Use It

How and where you use this depends on many, many factors. However, I'll make two suggestions so people who aren't familiar with CFCs.

Application.cfc

If you aren't using a framework, onApplicationStart() may be a good place for you to build your application configuration object. Be sure you understand the risks of using the Application scope on a shared hosting environment before considering letting this application live in the 'wild'. Below is some sample code which is executed the first (and every) time your application loads.

<cfcomponent output="false">

   <!--- START || onApplicationStart --->
   <cffunction name="onApplicationStart" returntype="boolean" output="false">
      
      <!--- Initialize Application Configuration Bean --->
      <cfset application.app = createObject("component","path.to.AppConfigService").init("/path/to/appConfig.xml.cfm")>

      <cfreturn true>

   </cfreturn>
   <!--- END || onApplicationStart --->

   <!--- START || onRequestStart --->
   <cffunction name="onRequestStart" returntype="boolean" output="false">
      
      <!--- Copy application bean for easier reference --->
      <cfset variables.app = application.app />

      <cfreturn true>

   </cfreturn>
   <!--- END || onRequestStart --->
   
</cfcomponent>

You'll also notice that the application bean is passed into the request so it can simply be called as app.getSite("name") or app.getMailAccounts("Admin").

onAppInit() (ColdBox/ColdSpring)

ColdBox has its own application/request/session facades (I _think_ it's safe to refer to them as such) which give me access to many of the powerful framework features. The two worth mentioning are the Autowire and IoC plugins. On my last project, I used Coldspring, Transfer and ColdBox together. In the ColdBox XML configuration file, you're able to identify an IoC framework that will be initialized when the application loads (prior to onAppInit()). Rather than manually instantiating my AppConfigService class, I simply declared it with all of my other objects in, you guessed it, the Coldspring XML configuration file. By the time onAppInit() fires, my service is already loaded in memory, I just need to find an "easy" way to access it. That's where the Autowire plugin comes into play. By convention, the Autowire plugin reads the metadata of your objects and based upon a property (<cfproperty>), it will retrieve objects from the cache (coldspring!). Since this topic is about my application bean, I won't go into detail on how to load coldspring and autowiring.

Similar to the Application.cfc example above, simply pass your application bean into your ColdBox cache, and reference it onRequest for simple use in your events!

UPDATED

The configuration object now allows for dynamically selected mail server settings and DSNs! Due to the reliability concerns of CGI.SERVER_NAME, I opted to identify the environment by the FQDN (fully qualified domain name). The <appHost> element is optional and will take precedence over the default settings. You may identify as many <appHost>s (aka. environments) as necessary.

Also, the AppConfigService now stores the FQDN of the server. You can retreive it as... app.getHostName()

4 responses so far ↓

Tony Garcia - Dec 16, 2008 at 9:16 PM

this is pretty cool. I have basically been using the coldbox.xml.cfm file for my app settings (in the custom settings section), but it hasn't "felt" right and I was thinking it might be a better idea to keep my app settings separate from my framework settings. So I might steal some of your ideas here...

Jason Durham - Dec 16, 2008 at 9:34 PM

Use away. That's what it's there for. :)

Let me know if I can help.

Nolan Erck - Dec 17, 2008 at 1:31 PM

Very nice! I've written/used similar things in the past, but they've been more domain specific. I just may have to swap out my code and drop in this CFC when time permits. :)

One issue I still run into is, when I have a config.xml file for Development, and slightly different ones for Staging and Production. (i.e. I use a different mail server and datasource for my Dev environment than my Production hosting company uses).

So basically I'm looking for a non-framework-specific way to never have to write this again:

<cfif cgi.server_name eq [Production URL goes here]>
<cfelseif cgi.server_name eq [Staging URL goes here]>

....you get the idea.

If you happen to come up with an enhancement to this CFC that makes dealing with those differences easier, please let me know.

Thanks for sharing!
-Nolan

Jason Durham - Dec 17, 2008 at 10:52 PM

@Nolan

I think I have your problem solved but went about it in a slightly different way. AppConfigService.getFQDN() uses a Java class to retrieve the hostname of the server. You can now identify different web hosts by surrounding mail server or db settings with <appHost>. The "name" attribute can contain a comma separated list.

HTH
Jason

Leave a Comment

Leave this field empty: