LightWire and Lighthouse Pro Application Configuration Example

2007 April 04
by Paul Marcotte
I see a lot of discussions centered around application configuration and the merits of different implementations. Of these, XML cofiguraiton is the most pervasive. A suggested best practice when using xml config files is to keep them outside of the web root to prevent exposing key settings. After recently installing Raymond Camden's Lighthouse Pro, I learned that you can also obfuscate the xml inside a comment block. To try and understand this clever workaround, I openend the hood (a.k.a Appplicaiton.cfc) and noted that with a very small change in onApplicationStart I could introduce both an IoC/DI container and swap out the xml for a programatic config using Peter Bell's LightWire.I set three goals for myself prior to starting this exercise: 1. minimize alterations to the code base. 2. keep the installation process virtually identical. 3. not alter the existing api. Step 1 : Adding LightWire to the mix LightWire, as the name impies, is more like a package than a framework. Indeed, aside from BeanConfig.cfc (the configuration file), there are only two files required to use LightWire. Since LightWire can be set up as a mapping, by placing the framework folder in the web root, or by creating a folder called lightwire at the application root and copying the core files (LightWire.cfc and BaseConfigObject.cfc) to the new folder. Note that this method required a change to the extends attribute of BeanConfig as it requires a fully qualified path from the web root. Step 2 : Creating the BeanConfig The LightWire BeanConfig file is where you define Constructor, Setter, or Mixin dependencies and properties for your Singleton and Transient objects. You can also create custom factories as Singletons and define both Singletons and Transients that are created from a factory method. To start, I defined each object that Lighthouse Pro creates in onApplicationStart. For brevity, I'll compare the code for a the UserDAO and UserManager objects. Original code: <cfset application.userDAO = createObject("component","components.UserDAO").init(    application.dsn,
application.username,
application.password )>

<cfset application.userManager = createObject("component","components.UserManager").init(   application.dsn,
application.username,
application.password,
application.userDAO    )>
BeanConfig code: // userDAO
addSingleton("lighthousepro.components.UserDAO","userDAO");
addConstructorProperty("userDAO","dsn",arguments.props.dsn);
addConstructorProperty("userDAO","username",arguments.props.username);
addConstructorProperty("userDAO","password",arguments.props.password);
// userManager

addSingleton("lighthousepro.components.UserManager","userManager");
addConstructorProperty("userManager","dsn",arguments.props.dsn);
addConstructorProperty("userManager","username",arguments.props.username);
addConstructorProperty("userManager","password",arguments.props.password);
addConstructorDependencies("userManager","userDAO");
There are two things to note about the differences between these examples. 1. the class path for the bean definition is the full class path from the web root. 2. the variable names used for the property values in BeanConfig are different. This is due to the fact that I am passing the default properties to the BeanConfig as a struct. Step 3 : Passing default properties to BeanConfig A BeanConfig file is customizable. I leveraged this fact to pass a struct of the default application settings to BeanConfig (similar to passing a struct to ColdSpring as default properties). This allowed me to maintain the defaults.cfm file, by switching from the xml settings <?xml version="1.0"?>
<initvals>
   <defaults>
      <adminemail>admin@localhost.com</adminemail>
      <dsn>lighthousepro</dsn>
      <username></username>
      <password></password>
      <dbtype>sqlserver</dbtype>
      <secretkey>wef320949879032dfhjhlds%^#</secretkey>
      <mailusername></mailusername>
      <mailpassword></mailpassword>
      <mailserver></mailserver>
      <mailport></mailport>
      <version>2.3.001</version>
   </defaults>
</initvals>
to the following... <cfscript>
// create a defaults struct to hold the values previously stored in defaults.cfm

defaults = StructNew();
// set defaults

defaults.adminemail = 'admin@localhost.com';
defaults.dsn = 'lighthousepro';
defaults.username = '';
defaults.password = '';
defaults.dbtype = 'sqlserver';
defaults.secretkey = 'wef320949879032dfhjhlds%^#';
defaults.mailusername = '';
defaults.mailpassword = '';
defaults.mailserver = '';
defaults.mailport = '';
defaults.attachmentpath = getDirectoryFromPath(getCurrentTemplatePath()) & "attachments";
defaults.version = '2.3.001';
</cfscript>
You'll notice that attachmentpath is now part of the default settings. This change was required to be able to pass the value as a property to BeanConfig. Step 4 : updating onApplicationStart to use LightWire At application startup, Lighthouse Pro reads the defaults.cfm files and parses the xml therein, creating an application scope key for each node under defaults. These keys are then used to instantiate the model objects. Here is a portion of the code: <cfset var xmlFile = "">
<cfset var xmlData = "">
<cfset var rootDir = getDirectoryFromPath(getCurrentTemplatePath())>
<cfset var key = "">
...      
<!--- load settings --->
<cffile action="read" file="#rootdir#/defaults.cfm" variable="xmlFile">
<!--- Remove comments --->
<cfset xmlFile = replace(xmlFile, "<!---","")>
<cfset xmlFile = trim(replace(xmlFile, "--->
"
,""))>
<cfset xmlData = xmlParse(xmlFile)>
<cfloop item="key" collection="#xmlData.initvals.defaults#">
   <cfset application[key] = xmlData.initvals.defaults[key].xmlText>
</cfloop>
Instead of reading the defaults file into an XML variable, onApplicationStart now includes defaults.cfm. The defaults struct is then passed to the init() method of BeanConfig, before passing BeanConfig to LightWire. <!--- load settings --->
<cfinclude template="defaults.cfm" />
<!--- create LightWire config bean and pass defaults struct --->
<cfset   beanConfig = CreateObject("component","BeanConfig").init(defaults)>
<!--- create a bean factory from config bean --->
<cfset    application.beanFactory = createObject("component","lightwire.LightWire").init(beanConfig)>
Step 5 : Creating application scope references to LightWire managed Singletons With a beanFactory to request objects from, it was easy for me to keep create application scope references to the LightWire managed Singletons (abbreviated example below). <!--- to preserve the original api, create application scope references for beans --->
<cfset application.utils = application.beanFactory.getSingleton("utils") />
<cfset application.userDAO = application.beanFactory.getSingleton("userDAO") />
<cfset application.userManager = application.beanFactory.getSingleton("userManager")>
...
Note that having a reference to LightWire as application.beanFactory negates the need to store other application scope references. I did this only to preserve the current api. The next step in harnessing the power of a IoC/DI engine is to reference it directly when requesting Singletons or Transients. Conclusion In all, the update affected only two files (Application.cfc and defaults.cfm) from Lighthouse Pro. In addition, only the LightWire framework (2 files) and a BeanConfig.cfc file were added to complete the re-configuration. I was able to meet the goals of this exercise without difficulty thanks to some very well crafted software...