Aaron Greenlee.com | A Personal Journey.
You should follow me here.
This is the RSS subscription you have been looking for.
 
Wrecking Ball Media
A great team that is clearing the way for digital marketing.

Have you thought about joining the Central Florida Web Developers User Group?

Application Constructor, a ColdBox Interceptor

POSTED Tuesday, May 11, 2010  |   | DOWNLOAD CODE
Keywords: ColdBox, ColdFusion, Interceptor, Design Pattern

Organizing code can be a challenge for even the smallest teams. Frameworks help us solve this problem by establishing conventions that bind us to expected behavior. This contract can greatly improve the quality and efficencity of your product/service. While there are many approaches to code organization (and frameworks), I most appreciate the ColdBox Framework. If you are not familure with ColdBox, it is CFML framework and toolkit that supports Adobe's propriatary ColdFusion CFML engine and two open source CFML engines Railo and OpenBD. ColdBox is an event-driven framework that offers 16 conventional interception points while supporting an infinite number of custom interception points you declare.

A ColdBox interception point allows a developer to encapsulate code in a single CFC that has methods that will be automatically executed at a specific moment in the request. For example, you can check to see if a user's subscription is active before every request and encapsulate the code into a '/interceptors/UserSubscriptionInterceptor.cfc'. You may then use the same file to penalize 30 days of service if a user is browsing using IE6. Now, all your User Subscription code is encapsulated in a single CFC regardless of when it needs to be executed. Furthermore, ColdBox implicitly calls your methods so long as you have registered the interceptor in the configuration file.

Real-World Example

I often make use of Interceptors to encapsulate code into an 'Application Constructor Interceptor' that ensures application settings are available for each request. The following examples load information into my application cache that rarely changes using two conventional interception points: 'afterConfigurationLoad' and 'preProcess'.

The Related Files in My Application

The constructor interceptor uses a Application DAO, Application Service the Constructor Interceptor itself and the ColdBox Config file to register the interceptor.
The following files are used in this technique:
/config/ColdBox.cfc
Registers the interceptor with ColdBox.
/interceptors/Constructor.cfc
The actual interceptor.
/model/DAO/ApplicationDAO.cfc
Responsible for fetching registry data from the database.
/model/objects/Registry.cfc
The Registry object.
/model/services/ApplicationService.cfc
Provides the API to fetch the registry.

The Constructor Interceptor

The 'afterConfigurationLoad' method is automatically executed by the ColdBox framework after the application's configuration file has been consumed by the framework but before other services of the framework are available (such as logging). At this point, I can fetch my application's registry from the database and persist in memory for quick access throughout the application (including other initialization tasks the application may require).

/interceptors/Constructor.cfc (afterConfigurationLoad)
<cffunction name="afterConfigurationLoad" returntype="void"
	hint="Initializes the GreenleeBlog application's registry.">
	<cfargument name="Event" />
	<cfargument name="interceptData" />		
	<cfscript>
		/* Construct the application registry */
		var data = getModel('ApplicationService').generateRegistry();
		getColdboxOCM().set('registry', data, getSetting('CacheApplicationTime'));			
	</cfscript>
</cffunction>

In the above example, I am not using the CFML Application Scope and have opted for the ColdBox Cache Manager to persist the data. As such, my data may not always exist. Furthermore, I don't know how long the data will persist since the value was defined in the application's configuration file. To ensure the registry data is avalible during this request I will allays confirm the existence of the 'registry' and reconstruct (if needed) at the 'preProcess' interception point.

/interceptors/Constructor.cfc (preProcess)
<cffunction name="preProcess" returntype="void"
	hint="Reloads the GreenleeBlog application if cache expired. Assigns a reference in private request collection for registry.">
	<cfargument name="Event" />
	<cfargument name="interceptData" />
	<cfscript>
		/* Use the private request collection for data not directly relevant to the visitor */
		var prc = event.getCollection( private = true );

		/*	Construct application registry if the cached version expired */  
		if (!getColdboxOCM().lookup('registry'))
			getColdboxOCM().set('registry', getModel('ApplicationService').generateRegistry(), getSetting('CacheApplicationTime'));
		
		/* Provide reference to registry */
		prc.registry = getColdboxOCM().get('registry');
	</cfscript>
</cffunction>

Depending on your application's requirements, you may not need to construct any data at the 'afterConfigurationLoad' interception point and can rely exclusively on the 'preProcess' interception point. For example, the keywords (or tags) in my blog application can only change when a new post is added. So, the following example persists two forms of keyword data in my cache: all keywords and top keywords. The first key 'keywords' is an array of all keywords (objects), sorted alphabetically. The second key, 'top_keywords', is an array of my most frequently used keywords (objects again) sorted in descending order. Some of the meta-keyword data in the head of this page expects this second 'top_keywords' array to exist. Both of these keys are required by my layout and possibly by any Events in the framework, so the code is encapsulated into my Constructor Interceptor and my 'preProcess' execution point.

/interceptors/Constructor.cfc (preProcess)
<cffunction name="preProcess" returntype="void"
	hint="Reloads the GreenleeBlog application if cache expired. Assignes a reference in private request collection for registry and keywords.">
	<cfargument name="Event" />
	<cfargument name="interceptData" />
	<cfscript>
		/* Use the private request collection for data not directly relevant to the visitor */
		var prc = event.getCollection( private = true );

		/*	Construct application registry if the cached version expired */  
		if (!getColdboxOCM().lookup('registry'))
			getColdboxOCM().set('registry', getModel('ApplicationService').generateRegistry(), getSetting('CacheApplicationTime'));
		
		/* Provide reference to registry */
		prc.registry = getColdboxOCM().get('registry');
		
		/* Construct keyword reference if the cached version expired */
		if (!getColdBoxOCM().lookup('keywords'))
			getColdboxOCM().set('keywords', getModel('BlogService').fetchKeywords(), getSetting('CacheApplicationTime'));
		
		prc.keywords = getColdboxOCM().get('keywords');
		
		/* Construct top keywords reference if the cached version expired */
		if (!getColdBoxOCM().lookup('top_keywords') OR !getColdBoxOCM().lookup('top_keywords_list')) {
			var kws = getModel('BlogService').fetchKeywords(sortByQuantity=true,max=getSetting('MaxTopKeywords'));
			var i = 0;
			var topkw = [];
			for (i=1; i <= arrayLen(kws); i++)
				arrayAppend(topkw, kws[i].getKeyword());

			getColdboxOCM().set('top_keywords', arrayToList(topkw, ', ') , getSetting('RSSFeedCacheTime'));
		}
		prc.topKeywords = getColdboxOCM().get('top_keywords');
	</cfscript>
</cffunction>

Following is the complete Constructor Interceptor code.

/interceptors/Constructor.cfc (Complete)
<cfcomponent extends="coldbox.system.Interceptor" output="false"
	hint="Application constructor used to persist slow data into memory.">
	
	<cffunction name="afterConfigurationLoad" returntype="void"
		hint="Initializes the GreenleeBlog application's registry.">
		<cfargument name="Event" />
		<cfargument name="interceptData" />		
		<cfscript>
			/* Construct the application registry */
			var data = getModel('ApplicationService').generateRegistry();
			getColdboxOCM().set('registry', data, getSetting('CacheApplicationTime'));			
		</cfscript>
	</cffunction>
	
	<cffunction name="preProcess" returntype="void"
		hint="Reloads the GreenleeBlog application if cache expired. Assigns a reference in private request collection for registry and keywords.">
		<cfargument name="Event" />
		<cfargument name="interceptData" />
		<cfscript>
			/* Use the private request collection for data not directly relevant to the visitor */
			var prc = event.getCollection( private = true );

			/*	Construct application registry if the cached version expired */  
			if (!getColdboxOCM().lookup('registry'))
				getColdboxOCM().set('registry', getModel('ApplicationService').generateRegistry(), getSetting('CacheApplicationTime'));
			
			/* Provide reference to registry */
			prc.registry = getColdboxOCM().get('registry');
			
			/* Construct keyword reference if the cached version expired */
			if (!getColdBoxOCM().lookup('keywords'))
				getColdboxOCM().set('keywords', getModel('BlogService').fetchKeywords(), getSetting('CacheApplicationTime'));
			
			prc.keywords = getColdboxOCM().get('keywords');
			
			/* Construct top keywords reference if the cached version expired */
			if (!getColdBoxOCM().lookup('top_keywords') OR !getColdBoxOCM().lookup('top_keywords_list')) {
				var kws = getModel('BlogService').fetchKeywords(sortByQuantity=true,max=getSetting('MaxTopKeywords'));
				var i = 0;
				var topkw = [];
				for (i=1; i <= arrayLen(kws); i++)
					arrayAppend(topkw, kws[i].getKeyword());

				getColdboxOCM().set('top_keywords', arrayToList(topkw, ', ') , getSetting('RSSFeedCacheTime'));
			}
			prc.topKeywords = getColdboxOCM().get('top_keywords');
		</cfscript>
	</cffunction>
	
</cfcomponent>

Related Files

Today's post will not go into the service layer in debth, but, I wanted to share the service and DAO to provide a complete view of my solution.

Database
A screenshot of the database.

The database schema is very simple and only includes three columns: ID, REFERENCE and PARENT.

/model/objects/Registry.cfc (Complete)
<cfcomponent name="Registry"
	hint="Application registry object for the Application.">
	
	<!--- Data --->
	<cfproperty name="values" type="string" />
	
	<cfscript>
		/* Construct */
		function init() {
			variables.values 		= { reference = {}, id = {} };
			return this;
		}
		
		function setValue(string reference, numeric id, numeric parent) {		
			variables.values.reference[arguments.reference] = { id = arguments.id, parent = arguments.parent};
			variables.values.id[arguments.id] = { id = arguments.reference, parent = arguments.parent};
		}
		function getValue(any key) {
			if (isNumeric(arguments.key))
				return variables.values.id[ arguments.key ];
			else
				return variables.values.reference[ arguments.key ];
		}
		function getValues(string method) {
			if (arguments.method == 'id')
				return variables.values.id;
			else
				return variables.values.reference;
		}
		function exists(any key) {
			return structKeyExists(variables.values.id, arguments.key) && structKeyExists(variables.values.reference, arguments.key);
		}
	</cfscript>
</cfcomponent>
/model/service/ApplicationService.cfc (Complete)
<cfcomponent name="ApplicationService" autowire="true" hint="Supports the construction of the application.">
	
	<!--- AUTOWIRE PROPERTIES TO BE INJECTED BY COLDBOX --->
	
	<cfproperty name="DatasourceBean"
				type="coldbox:datasource:applicationData"
				scope="variables.dependency" />
				
	<cfproperty name="query_cache"
				type="coldbox:setting:query_cache"
				scope="variables.dependency" />
				
	<cfproperty name="BeanFactory"
				type="coldbox:plugin:beanfactory"
				scope="variables.dependency" />
				
	<cfproperty name="ApplicationDAO"
				type="model:ApplicationDAO"
				scope="variables.dependency" />

	<cffunction name="generateRegistry" access="public" hint="Returns a constructed Registry object.">
		<cfset var Registry = createObject('component', 'model.objects.Registry').init() />
		<cfreturn variables.dependency.ApplicationDAO.fetchRegistry(Registry) /> 
	</cffunction>

</cfcomponent>
/model/DAO/ApplicationDAO.cfc (Complete)
<cfcomponent name="BlogDAO" autowire="true" hint="Returns properties that change infrequently. Useful during application construction.">
	
	<!--- AUTOWIRE PROPERTIES TO BE INJECTED BY COLDBOX --->
	<cfproperty name='DatasourceBean'
				type='coldbox:datasource:applicationData'
				scope='variables.dependency' />
				
	<cfproperty name="BeanFactory"
				type="coldbox:plugin:beanfactory"
				scope="variables.dependency" />

	<!--- CONSTRUCTOR --->
	<cffunction name="init" access="public">
		<cfreturn this />
	</cffunction>
	
	<!--- PUBLIC --->
	<cffunction name="fetchRegistry" access="public" returntype="struct"
		hint="Reads the registry and returns a struct of keys and a struct of ids to allow two methods of accessing the data.">
		<cfargument name="Registry" hint="The application's registry object." />
		
		<cfset var q = queryNew('') />
		<cfquery name="q" datasource="#variables.dependency.DatasourceBean.getName()#"
			username="" password="">
			SELECT id, reference, parent FROM registry
		</cfquery>
		
		<!--- Populate Registry --->
		<cfloop query="q">
			<cfset Registry.setValue(reference 	= q.reference, id = q.id, parent = q.parent) />
		</cfloop>
		
		<cfreturn Registry />
	</cffunction>
</cfcomponent>
blog comments powered by Disqus
You can send me email or work with
   me for digital marketing, web design
      and application development.

         I own proprietary web application development
         company and work with a leading digital marketing firm.
© 2009 - 2012 Aaron Greenlee. Powered by my own code on the ColdBox Framework.

This site is best viewed on Chrome, FireFox and Safari. Subscribe to my RSS feed.