Monday, September 21, 2009

ColdFusion and Frameworks

If you haven't read it yet, I would suggest reading this post by Matt Woodward. Very good points.

Honestly, if ColdFusion has been around for almost 15 years, why don't we have anything that comes even remotely close to Ruby on Rails or Grails? You could make the case that ColdFusion on Wheels and ColdBox are similar in their implementations, but then there are people that swear by Model-Glue, Mach-II, and Fusebox... and then there's Edmund, FW/1, onTap, etc... and those are just the front controller frameworks. If you want the total package, you'll have to wire in ColdSpring, LightWire, Transfer, Reactor, MXUnit, cfcUnit, cfSpec, etc...

Unfortunately, the ColdFusion framework community is so fragmented that I don't think there will ever be a widely-accepted, well-designed, dominant framework - a framework that just works - a framework that some developers might actually confuse with a language, like what happens with Rails(Ruby) and Grails(Groovy). Right now there are just too many smart ColdFusion developers all working on their own versions of the same thing.

Tuesday, September 1, 2009

Annotation-based Dependency Injection using ColdSpring

ColdSpring is by far the most essential tool I need for building ColdFusion applications. However, I'm not a huge fan of writing a ton of XML and I'm certainly not a fan of writing a lot of getters and setters inside my components.

To help ease that pain, I borrowed a feature from Model-Glue 3 and extended ColdSpring to create the "beans" scope.

Little did I realize ColdSpring can already handle a lot of what I wanted to do without having to modify ColdSpring itself simply by creating a factory post processor. To automatically inject the "beans" scope into my ColdSpring-managed beans, I created the following BeansScopeFactoryPostProcessor.cfc:


<cfcomponent>

<cffunction name="postProcessBeanFactory" access="public" returntype="void">

<cfset var local = {} />

<cfset local.beanDefs = getConcreteBeanClasses() />

<cfloop collection="#local.beanDefs#" item="local.beanName">

<cfset local.beanList = getBeanScopeList(local.beanDefs[local.beanName]) />

<cfif local.beanList neq "">

<cfset local.bean = getBeanFactory().getBean(local.beanName) />

<cfset injectBeansScope(local.bean,local.beanList) />

</cfif>

</cfloop>

</cffunction>

<cffunction name="getConcreteBeanClasses" access="private" returntype="struct">

<cfset var local = {} />

<cfset local.classes = {} />

<cfset local.beanDefs = getBeanFactory().getBeanDefinitionList() />

<cfloop collection="#local.beanDefs#" item="local.beanName">

<cfif not local.beanDefs[local.beanName].isAbstract()>
<cfset local.classes[local.beanName] = local.beanDefs[local.beanName].getBeanClass() />
</cfif>

</cfloop>

<cfreturn local.classes />

</cffunction>

<cffunction name="injectBeansScope" access="private" returntype="void">
<cfargument name="bean" required="true" />
<cfargument name="beanList" required="true" />

<cfset var local = {} />

<cfif isCFC(arguments.bean)>

<cfset local.beans = {} />

<cfloop list="#arguments.beanList#" index="local.beanName">
<cfset local.beans[local.beanName] = getBeanFactory().getBean(local.beanName) />
</cfloop>

<cfset arguments.bean.__setVariable = variables.__setVariable />

<cfset arguments.bean.__setVariable("beans",local.beans) />

<cfset StructDelete(arguments.bean,"__setVariable") />

</cfif>

</cffunction>

<cffunction name="getBeanScopeList" access="private" returntype="string">
<cfargument name="bean" required="true" />

<cfset var local = {} />

<cfset local.metaData = getComponentMetaData(arguments.bean) />

<cfset local.beanList = "" />

<cfif StructKeyExists(local.metaData,"beans")>

<cfloop list="#local.metaData.beans#" index="local.beanName">
<cfset local.beanList = ListAppend(local.beanList,local.beanName) />
</cfloop>

</cfif>

<cfset local.extendedMetaData = local.metaData />

<cfloop condition="StructKeyExists(local.extendedMetaData,'extends')">

<cfif StructKeyExists(local.extendedMetaData,"beans")>

<cfloop list="#local.extendedMetaData.beans#" index="local.beanName">

<cfif not listFindNoCase(local.beanList,local.beanName)>
<cfset local.beanList = ListAppend(local.beanList,local.beanName) />
</cfif>

</cfloop>

</cfif>

<cfset local.extendedMetaData = local.extendedMetaData.extends />

</cfloop>

<cfreturn local.beanList />

</cffunction>

<cffunction name="setBeanFactory" access="public" returntype="void">
<cfargument name="beanFactory" required="true" type="coldspring.beans.BeanFactory" />

<cfset variables.beanFactory = arguments.beanFactory />

</cffunction>

<cffunction name="getBeanFactory" access="private" returntype="any">

<cfreturn variables.beanFactory />

</cffunction>

<cffunction name="__setVariable" access="public" returntype="void">
<cfargument name="key" required="true" />
<cfargument name="value" required="true" />

<cfset variables[arguments.key] = arguments.value />

</cffunction>

<cffunction name="isCFC" access="private" returntype="boolean">
<cfargument name="object" required="true" />

<cfset var metaData = getMetaData(arguments.object) />

<cfreturn isObject(arguments.object) and structKeyExists(metaData,"type") and metaData.type eq "component" />

</cffunction>

</cfcomponent>


Long story short, it looks at all the beans defined in ColdSpring, checks their metadata for a "beans" attribute inside the cfcomponent tag, and automatically injects the requested beans into the component inside a variables.beans struct.

To get ColdSpring to process my beans, I define the factory post processor as such:


<bean id="beanInjector" class="test.BeansScopeFactoryPostProcessor" factory-post-processor="true" />


For those unaware of how factory post-processors work, ColdSpring will look for any beans where factory-post-processor="true" and automatically call postProcessBeanFactory() on those beans once the bean factory has been initialized.

To keep the code relatively small in this post, I removed any comments. Hopefully it's still somewhat straight-forward and easy to follow.