Tuesday, December 30, 2008

Model-Glue-esque Bean Factory, Redux

After using my latest attempt at a Model-Glue inspired bean factory a little longer, I ran into a major problem: circular dependencies.

In my application (and most others), services will need to be able to talk to each other. For example, my application has a productService and a categoryService, and each service will need an instance of the other injected into it.

In ColdSpring, you can get around circular dependencies by using properties (setter injection) rather than constructor arguments. However, I want to use the "beans" scope to inject all my dependencies rather than having to create the necessary setProductService() and setCategoryService() methods.

I solved the problem by re-working my DynamicBeanFactory. I updated the init() method to construct all the beans when the bean factory is created, then inject the "beans", "helpers" and "includes" as necessary. Doing this also allowed me to leave the getBean() method alone, which is kinda nice.

Here's the updated DynamicBeanFactory:


<cfcomponent output="false" extends="coldspring.beanutils.DynamicXMLBeanFactory">

<!------>

<cffunction name="init" access="public" output="false" returntype="any">
<cfargument name="pathToColdspringXML" required="true" type="string" />
<cfargument name="params" required="false" default="" />

<cfset var local = StructNew() />
<cfset var i = "" />

<!--- dynamic properties that get passed into the bean factory --->
<cfif not isStruct(arguments.params)>
<cfset arguments.params = StructNew() />
</cfif>

<!--- init the bean factory --->
<cfset super.init() />

<!--- load the beans --->
<cfset loadBeansFromDynamicXmlFile(arguments.pathToColdspringXML,arguments.params) />

<!--- construct each bean, which puts it in the singleton cache --->
<cfloop collection="#variables.beanDefs#" item="i">
<cfset constructBean(i) />
</cfloop>

<!--- if a "helpers" bean is defined, inject it into all the helpers and beans --->
<cfif StructKeyExists(variables.singletonCache,"helpers")>

<cfset local.helpers = variables.singletonCache.helpers.getHelpers(this) />

<cfset local.injectedHelpers = StructNew() />

<cfloop collection="#local.helpers#" item="i">

<cfset local.bean = local.helpers[i] />

<!--- inject a function to set variables into the "variables" scope --->
<cfset local.bean.__injectVariable = variables.injectVariable />

<!--- inject the "helpers" scope --->
<cfset local.bean.__injectVariable("helpers",local.injectedHelpers) />

<!--- remove the temporary inject function --->
<cfset StructDelete(local.bean,"__injectVariable") />

<cfset local.injectedHelpers[i] = local.bean />

</cfloop>

</cfif>

<cfloop collection="#variables.singletonCache#" item="i">

<!--- set the bean from the cache --->
<cfset local.bean = variables.singletonCache[i] />

<!--- inject a function to set variables into the "variables" scope --->
<cfset local.bean.__injectVariable = variables.injectVariable />

<!--- inject the "helpers" scope if it's defined --->
<cfif StructKeyExists(variables.singletonCache,"helpers")>
<cfset local.bean.__injectVariable("helpers",local.injectedHelpers) />
</cfif>

<!--- get the bean's meta data to see if there are any "beans" or "includes" defined --->
<cfset local.metaData = getMetaData(local.bean) />

<!--- create a container for all the beans that will be injected --->
<cfset local.beans = StructNew() />

<!--- check to see if the bean needs to inject any beans --->
<cfif StructKeyExists(local.metaData,"beans")>

<!--- include each bean in the order specified --->
<cfloop list="#local.metaData.beans#" index="local.injectedBeanName">

<cfset local.injectedBeanName = trim(local.injectedBeanName) />

<!--- insert each bean into the local beans struct, which will be injected after the loop --->
<cfset local.beans[local.injectedBeanName] = variables.singletonCache[local.injectedBeanName] />

</cfloop>

</cfif>

<!--- inject the "beans" scope --->
<cfset local.bean.__injectVariable("beans",local.beans) />

<!--- check for any included beans --->
<cfif StructKeyExists(local.metaData,"includes")>

<!--- include each bean in the order specified --->
<cfloop list="#local.metaData.includes#" index="local.includedBeanName">

<cfset local.includedBeanName = trim(local.includedBeanName) />

<!--- get the bean from cache --->
<cfset local.includedBean = variables.singletonCache[local.includedBeanName] />

<!--- get all the public functions in the included bean and copy them to the main bean --->
<!--- even though private functions aren't copied, references to them from within copied functions will still work --->
<cfloop collection="#local.includedBean#" item="local.includedBeanVariableName">

<!--- only copy the function if it isn't defined yet --->
<cfif not StructKeyExists(local.bean,local.includedBeanVariableName)>

<!--- put the function into the object (the "this" scope) --->
<cfset local.bean[local.includedBeanVariableName] = local.includedBean[local.includedBeanVariableName] />

<!--- set the function into the variables scope, otherwise would need to reference the function as this.fn() --->
<cfset local.bean.__injectVariable(local.includedBeanVariableName,local.includedBean[local.includedBeanVariableName]) />

</cfif>

</cfloop>

</cfloop>

</cfif>

<!--- remove the temporary inject function --->
<cfset StructDelete(local.bean,"__injectVariable") />

</cfloop>

<cfreturn this />

</cffunction>

<!------>

<cffunction name="injectVariable" access="public" output="false" returntype="void" hint="I get injected into a bean in order to set variables">
<cfargument name="key" required="true" />
<cfargument name="value" required="true" />

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

<cfreturn />

</cffunction>

<!------>

</cfcomponent>

Monday, December 22, 2008

Creating a Model-Glue-esque Bean Factory

I've been playing around with Model-Glue 3 lately and found that there are some really nice features that eliminate a lot of repetitive code, such as automatically injecting helpers and beans into a Controller without having to create getters and setters.

One problem I had with these new features is that they're only available to Model-Glue controllers (obviously) and I want to use them in my model and service layer. And so it began...

My first implementation required each object to extend a base object, which worked, but didn't feel quite right; there was still too much repetitive code. Plus I don't really like extending objects directly since it binds each object to a single implementation of the extended object. I'll get into that a little later.

After playing around with things a little more, I found a way that still works, is a lot cleaner, feels a lot better, and has an added bonus.

The first thing I did was create a custom bean factory that extends Brian Kotek’s DynamicXmlBeanFactory extension for ColdSpring (http://coldspringutils.riaforge.org/) and overrides the getBean method.

My bean factory is created onApplicationStart() like this:


<cfset application.beanFactory = createObject("component", "coldspring.beanutils.DynamicBeanFactory").init("config/coldspring/beans.xml", application.config) />


My bean factory looks like this:


<cfcomponent output="false" extends="coldspring.beanutils.DynamicXMLBeanFactory">

<!------>

<cffunction name="init" access="public" output="false" returntype="any">
<cfargument name="pathToColdspringXML" required="true" type="string" />
<cfargument name="params" required="false" default="" />

<cfif not isStruct(arguments.params)>
<cfset arguments.params = StructNew() />
</cfif>

<cfset super.init() />
<cfset loadBeansFromDynamicXmlFile(arguments.pathToColdspringXML,arguments.params) />

<cfreturn this />

</cffunction>

<!------>

<cffunction name="getBean" access="public" output="false" returntype="any">
<cfargument name="beanName" required="true" />

<cfset var local = StructNew() />

<!--- get the bean from coldspring --->
<cfset local.bean = super.getBean(arguments.beanName) />

<!--- inject the bean with helpers, beans, includes, etc... --->
<cfinclude template="DynamicBeanFactoryMixin.cfm" />

<cfreturn local.bean />

</cffunction>

<!------>

<cffunction name="injectVariable" access="public" output="false" returntype="void" hint="I get copied into beans in order to set variables">
<cfargument name="key" required="true" />
<cfargument name="value" required="true" />

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

<cfreturn />

</cffunction>

<!------>

</cfcomponent>


You'll notice my getBean method includes DynamicBeanFactoryMixin.cfm, which is responsible for injecting additional logic into my bean. I'm not sold on using a .cfm include, but I wanted to re-use my code in another component that I've been working on and I didn't want to duplicate my efforts. I figure I can always refactor later, but wanted to get the base functionality down.

The DynamicBeanFactoryMixin looks like this:


<!--- inject a function to set variables into the "variables" scope --->
<cfif StructKeyExists(variables,"injectVariable")>
<cfset local.bean.injectVariable = variables.injectVariable />
<cfelseif StructKeyExists(this,"injectVariable")>
<cfset local.bean.injectVariable = this.injectVariable />
</cfif>

<cfif StructKeyExists(variables,"helpers")>

<!--- if the helpers are already defined in the current bean, set them into the target --->
<cfset local.bean.injectVariable("helpers",variables.helpers) />

<cfelse>

<!--- inject the helpers and create the "helpers" scope --->
<cfset local.bean.injectVariable("helpers",super.getBean("helpers").getHelpers()) />

</cfif>

<cfset local.metaData = getMetaData(local.bean) />

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

<!--- include each bean in the order specified --->
<cfloop list="#local.metaData.includes#" index="local.includedBeanName">

<cfset local.includedBeanName = trim(local.includedBeanName) />

<cfif StructKeyExists(variables,"getBean")>
<cfset local.includedBean = getBean(local.includedBeanName) />
<cfelse>
<cfset local.includedBean = application.beanFactory.getBean(local.includedBeanName) />
</cfif>

<!--- get all the public functions in the included bean and copy them to the main bean --->
<!--- even though private functions aren't copied, references to them from within copied functions will still work --->
<cfloop collection="#local.includedBean#" item="local.includedBeanVariableName">

<!--- only copy the function if it isn't defined yet --->
<cfif not StructKeyExists(local.bean,local.includedBeanVariableName)>

<!--- put the function into the object (the "this" scope) --->
<cfset local.bean[local.includedBeanVariableName] = local.includedBean[local.includedBeanVariableName] />

<!--- set the function into the variables scope, otherwise would need to reference the function as this.fn() --->
<cfset local.bean.injectVariable(local.includedBeanVariableName,local.includedBean[local.includedBeanVariableName]) />

</cfif>

</cfloop>

</cfloop>

</cfif>

<!--- create a container for all the beans that will be injected --->
<cfset local.beans = StructNew() />

<!--- check to see if the bean needs to inject any beans --->
<cfif StructKeyExists(local.metaData,"beans")>

<!--- include each bean in the order specified --->
<cfloop list="#local.metaData.beans#" index="local.injectedBeanName">

<cfset local.injectedBeanName = trim(local.injectedBeanName) />

<!--- if getBean is defined, use it --->
<cfif StructKeyExists(variables,"getBean")>
<cfset local.injectedBean = getBean(local.injectedBeanName) />
<cfelse>
<cfset local.injectedBean = application.beanFactory.getBean(local.injectedBeanName) />
</cfif>

<!--- insert each bean into the local beans struct, which will be injected after the loop --->
<cfset local.beans[local.injectedBeanName] = local.injectedBean />

</cfloop>

</cfif>

<!--- inject the beans --->
<cfset local.bean.injectVariable("beans",local.beans) />


I'll try to describe what's going on here.

First, I copy a reference to an "injectVariable" function into the bean that allows me to inject variables (helpers, beans, etc...) into the variables scope of the target bean.

Next, I create the "helpers" scope inside the bean by using a HelperInjector extension for ColdSpring that I wrote. Here's how I define my helpers in ColdSpring:


<bean id="helpers" class="coldspring.beanutils.HelperInjector">
<property name="directories">
<list>
<value>/clipboard/app/helpers/</value>
</list>
</property>
<property name="files">
<map>
<entry key="date"><value>/clipboard/app/helpers/date.cfc</value></entry>
</map>
</property>
</bean>


Helpers can be passed in as a list (array) of directories or a map (struct) of individual CFCs. This is a little different than the viewMappings node in Model-Glue, but I think it's a little more flexible since you can include, exclude, and override specific components.

In case you're curious, the HelperInjector extension looks like this:


<cfcomponent name="HelperInjector" output="false">

<!------>

<cffunction name="init" access="public" output="false" returntype="any">

<cfset variables.instance = StructNew() />

<cfset setDirectories(ArrayNew(1)) />
<cfset setFiles(StructNew()) />

<cfreturn this />

</cffunction>

<!------>

<cffunction name="setDirectories" access="public" output="false" returntype="void">
<cfargument name="directories" required="true" />

<cfif isArray(arguments.directories)>
<cfset variables.instance.directories = arguments.directories />
</cfif>

</cffunction>

<!------>

<cffunction name="getDirectories" access="public" output="false" returntype="any">
<cfreturn variables.instance.directories />
</cffunction>

<!------>

<cffunction name="setFiles" access="public" output="false" returntype="void">
<cfargument name="files" required="true" />

<cfif isStruct(arguments.files)>
<cfset variables.instance.files = arguments.files />
</cfif>

</cffunction>

<!------>

<cffunction name="getFiles" access="public" output="false" returntype="any">
<cfreturn variables.instance.files />
</cffunction>

<!------>

<cffunction name="getHelpers" access="public" output="false" returntype="any">

<cfif not StructKeyExists(variables.instance,"helpers")>
<cfset setHelpers() />
</cfif>

<cfreturn variables.instance.helpers />

</cffunction>

<!------>

<cffunction name="setHelpers" access="public" output="false" returntype="any">

<cfset var directories = getDirectories() />
<cfset var directory = "" />
<cfset var files = "" />
<cfset var file = "" />
<cfset var i = "" />

<cfset variables.instance.helpers = StructNew() />

<cfif ArrayLen(directories) neq 0>

<cfloop from="1" to="#ArrayLen(directories)#" index="i">

<cfset directory = directories[i] />

<cfdirectory action="list" directory="#expandPath(directory)#" name="files">

<cfloop query="files">
<cfif listLast(files.name,".") eq "cfc">
<cfset setHelper(listFirst(files.name,"."),injectComponent(directory & "/" & files.name)) />
</cfif>
</cfloop>

</cfloop>

</cfif>

<cfset files = getFiles() />

<cfloop collection="#files#" item="i">

<cfif listLast(files[i],".") eq "cfc">
<cfset setHelper(i,injectComponent(files[i])) />
</cfif>

</cfloop>

</cffunction>

<!------>

<cffunction name="setHelper" access="private" output="false" returntype="any">
<cfargument name="key" required="true" />
<cfargument name="value" required="true" />

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

<cfreturn />

</cffunction>

<!------>

<cffunction name="injectComponent" access="private" output="false" returntype="any">
<cfargument name="path" required="true" />

<cfset var helperPath = expandPath(arguments.path) />
<cfset var helperFileName = listFirst(getFileFromPath(helperPath), ".") />
<cfset var componentName = replaceNoCase(arguments.path, "/", ".", "all") />
<cfset var instance = "" />

<cfif left(componentName, 1) eq ".">
<cfset componentName = right(componentName, len(componentName) - 1) />
</cfif>

<cfset componentName = listDeleteAt(componentName, listLen(componentName, "."), ".") />

<cfset instance = createObject("component", componentName) />

<cfreturn instance />

</cffunction>

<!------>

</cfcomponent>


Next, I check the bean's metadata for an "includes" attribute, which is similar to the "extends" attribute but with a twist.

When you extend a component, you must specify the full path to the CFC, such as extends="coldspring.beanutils.DynamicXMLBeanFactory", which directly binds your component to the implementation of another component, which doesn't lend itself towards loose coupling. Plus, you can only extend a single component.

With the "includes" attribute, you can specify a comma-separated list of ColdSpring beans that you want to extend. That way, the dependencies are still managed in ColdSpring. If you decide to switch implementations of a component, you only have to update the class path in your bean definition to point to a different CFC; the rest of your code remains unchanged. It's almost like extending an interface. Also, it allows you to extend multiple beans, which could come in handy and help keep your class files smaller.

I've been using the "includes" attribute in my DAOs and beans in order to include generic functionality, such as creating a new instance of a bean or creating implicit getters and setters by using onMissingMethod(). Doing this allows my beans to be a simple list of properties like this:


<cfcomponent name="Product" table="products" includes="bean" output="false">

<cfproperty name="id" type="string" default="" />
<cfproperty name="name" type="string" default="" />
<cfproperty name="abbrev" type="string" default="" />

</cfcomponent>


Hopefully that makes sense. I'm still working on a DynamicTransientFactory extension and DynamicBean extension, so I'll go into more details in a future post. I just thought it might explain how the "includes" attribute works better if I provided an example.

Finally, I create the "beans" scope inside the target bean, which allows me to specify which beans I need in the component's metadata without having to create getters and setters for each injected bean. This also keeps my ColdSpring XML file smaller since I don't need to specify all the dependencies between beans. I tried keeping the functionality as similar as possible to the Model-Glue 3 implementation, since I really liked how that worked.

All in all, I'm quite happy with the way things have turned out so far. I had a little trouble with the .cfm mixin and getting it to work with the "includes" attribute, so I had to hack a couple things like referencing application.beanFactory directly and requiring a "helpers" bean to be defined. But when it came down to it, I decided to go with convention over configuration.

I tried commenting the code where I could, so hopefully it makes sense. I haven't fully tested everything yet, but it seems to work so far. Also, I should note the code requires ColdFusion 8.

UPDATE: I re-worked the bean factory.

Tuesday, December 16, 2008

Dynamic config variables in Model-Glue

I've been working on a project at work called Clipboard that tracks RFP text by product and category. I recently updated the application to use Model-Glue 3 and ColdSpring, but was having trouble managing the Model-Glue configuration settings between my development environment and our beta environment. I'd like to deploy the project from one environment to the next without having to change any XML, but didn't quite know how to use dynamic variables inside the Model-Glue XML file. I finally solved the problem using a combination of techniques.

For starters, I'm running ColdFusion 8 and my project setup looks like this:



I’m using the Environment Config project (http://environmentconfig.riaforge.org/) to manage my variables between environments. This XML file returns a struct of variables depending on the environment you’re currently in, which can then be passed into ColdSpring as a set of default properties.

I’m also using Brian Kotek’s DynamicXmlBeanFactory extension for ColdSpring (http://coldspringutils.riaforge.org/) which extends the standard DefaultXMLBeanFactory but allows the ability to replace dynamic properties anywhere in the XML file, as well as in imported XML files.

By combining the Environment Config with the DynamicXmlBeanFactory, I’m able to pass environment-specific variables into ColdSpring and have ColdSpring evaluate them at runtime.

My onApplicationStart() method looks like this:

<cffunction name="onApplicationStart" output="false">

<cfset this.mappings["clipboard"] = expandPath(".") />
<cfset this.mappings["modelglue"] = expandPath("../modelglue/") />
<cfset this.mappings["coldspring"] = expandPath("../coldspring/") />

<cfset application.config = createObject("component","clipboard.config.environment.environment").init("config/environment/environment.xml.cfm").getEnvironmentByUrl(cgi.server_name) />

<cfset application.config.siteroot = getDirectoryFromPath(getMetaData().path) />

<cfset application.beanFactory = createObject("component","coldspring.beanutils.DynamicXmlBeanFactory").init() />
<cfset application.beanFactory.loadBeansFromDynamicXmlFile("config/coldspring/beans.xml",application.config) />

</cffunction>
Pretty basic stuff. I'm create mappings for my project, ColdSpring, and Model-Glue, create a struct containing my environment's configuration settings, create a dynamic file path to my project (for uploads), then create my bean factory.

Now on to Model-Glue. Model-Glue uses ColdSpring to manage its internal configuration. With each Model-Glue project, there’s a modelglue.modelGlueConfiguration bean that holds the configuration settings for Model-Glue, such as whether to display debug information, the default event, and paths to your views. It also has a property called “reload” that tells your application whether it should load Model-Glue with each request. This is something you’d want to do in a development environment, but not production as it may take a second to fully load Model-Glue.

Unfortunately, you aren’t able to use dynamic variables inside the Model-Glue XML config file by default, so you’re stuck with having to either manually change the “reload” variable or create an ANT script to modify the XML file with each deployment. Yuck.

Fortunately, there’s a nice little trick I found that will allow you to use dynamic variables. There are a couple lines in the Model-Glue index.cfm file that are commented out by default. The ones we care about are ModelGlue_LOCAL_COLDSPRING_PATH, which specifies where the Model-Glue config file is located, and ModelGlue_PARENT_BEAN_FACTORY, which can be used to specify a parent bean factory to Model-Glue.

Simply put, Model-Glue will look for beans inside its bean factory first (created using ModelGlue_LOCAL_COLDSPRING_PATH), and if it can’t find a bean there, it will check the parent bean factory. Knowing this, we can move the modelglue.modelGlueConfiguration bean out of the Model-Glue bean factory and into the parent bean factory, where we can use the combination of Environment Config and DynamicXmlBeanFactory to use dynamic variables.

Here’s what my index.cfm file looks like:
<cfsilent>
<cfset ModelGlue_LOCAL_COLDSPRING_PATH = "config\modelglue\empty.xml" />
<cfset ModelGlue_PARENT_BEAN_FACTORY = application.beanFactory />
</cfsilent><cfinclude template="../modelglue/gesture/modelglue.cfm" />
You’ll notice my ModelGlue_LOCAL_COLDSPRING_PATH is set to empty.xml, which contains just <beans></beans> so Model-Glue won’t error out.

Here’s what config/environment/environment.xml.cfm looks like:
<?xml version="1.0" encoding="UTF-8"?>
<environments>
<default>
<config>
<property name="development">false</property>
<property name="webpath">/clipboard</property>
</config>
</default>
<environment id="development">
<patterns>
<pattern>localhost</pattern>
</patterns>
<config>
<property name="datasource">Clipboard_Dev</property>
<property name="development">true</property>
</config>
</environment>
<environment id="beta">
<patterns>
<pattern>betasite.com</pattern>
</patterns>
<config>
<property name="datasource">Clipboard_Beta</property>
</config>
</environment>
</environments>
Notice I have a property named “development” that is set to “false” by default, but “true” inside my “development” environment. When I load up my application, I use this XML to create an application.config variable, which is then passed into my dynamic ColdSpring bean factory.

Here’s what my config/coldspring/modelglue.xml file looks like, which now needs to be imported into my beans.xml file as part of Model-Glue's parent bean factory:
<beans>

<!-- This is your Model-Glue configuration -->
<bean id="modelglue.modelGlueConfiguration" class="ModelGlue.gesture.configuration.ModelGlueConfiguration">

<!-- Be sure to change these to false when you go to production! -->
<property name="reload"><value>${development}</value></property>
<property name="debug"><value>false</value></property>

<!-- Default event-handler -->
<property name="defaultEvent"><value>index</value></property>
<property name="missingEvent"><value>missing</value></property>
<property name="defaultExceptionHandler"><value>error</value></property>

<!-- Controls reloading -->
<property name="reloadPassword"><value>true</value></property>
<property name="reloadKey"><value>init</value></property>

<!-- Where to find necessary files -->
<property name="configurationPath"><value>config/modelglue/modelglue.xml</value></property>
<property name="applicationPath"><value>${webpath}</value></property>
<property name="viewMappings"><value>${webpath}/app/views</value></property>
<property name="helperMappings"><value>${webpath}/app/helpers</value></property>

<!-- Generate unknown events when in development mode? (reload=false) -->
<property name="generationEnabled"><value>false</value></property>
</bean>

</beans>
Notice my “reload” property is now set to ${development}, which gets evaluated to true/false when the bean factory is created in my Application.cfc. I’ve also made a couple other variables dynamic using my ${webpath} variable, which could change from one environment to another.