Custom App XML Configuration File
Posted by Jason Durham | Tags: ColdFusion , ColdBox
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 ↓
Leave a Comment