Wednesday, November 11, 2009

ORM Event Handling in ColdFusion 9

I've been playing around with ORM event handling in ColdFusion 9. For my application, I want to be able to fire events during various points in the lifecyle of my persistent entities. However, I don't want to have to clutter up my business logic with tasks such as logging, synchronizing integration data, or pushing changes out to Flex. I want to take a more AOP-style approach.

To do this, I created an XML-driven EventManager to use as my application's global event handler. With my EventManager, I'm able to configure listeners to respond to events fired by my entities using the built in ORM event handlers. The events can be registered using regular expressions and all follow the pattern {EntityName}:{EventHandler}. For example, I might have User:postUpdate or Product:postDelete.

In my sample application, I've configured my EventManager to send an email before and after any entity is loaded, inserted, updated, or deleted. In other words, basically anytime something happens to an entity. I'm not sure how useful this would be, but it's just an example.

Here's the directory structure:



Application.cfc

component {

this.name = "Sample";
this.datasource = "Sample";
this.mappings["/sample"] = getDirectoryFromPath(getCurrentTemplatePath());

this.ormEnabled = true;
this.ormSettings.dbcreate = "update";
this.ormSettings.eventHandling = true;
this.ormSettings.eventHandler = "sample.com.EventHandler";

public void function onApplicationStart() {

application.beanFactory = createObject("component","coldspring.beans.DefaultXmlBeanFactory").init();

application.beanFactory.loadBeans("/sample/config/coldspring.xml");

}

}


EventHandler.cfc

component implements="cfide.orm.IEventHandler" {

public void function preLoad(any entity) {
handleEvent(entity,"preLoad");
}

public void function postLoad(any entity) {
handleEvent(entity,"postLoad");
}

public void function preInsert(any entity) {
handleEvent(entity,"preInsert");
}

public void function postInsert(any entity) {
handleEvent(entity,"postInsert");
}

public void function preUpdate(any entity, struct oldData) {
handleEvent(entity,"preUpdate");
}

public void function postUpdate(any entity) {
handleEvent(entity,"postUpdate");
}

public void function preDelete(any entity) {
handleEvent(entity,"preDelete");
}

public void function postDelete(any entity) {
handleEvent(entity,"postDelete");
}

private void function handleEvent(any entity, string handler) {

var collection = {};
collection.entity = ormGetSession().getEntityName(entity);
collection.id = entity.getID();
collection.handler = handler;

var eventManager = application.beanFactory.getBean("eventManager");
eventManager.dispatchEvent("#collection.entity#:#collection.handler#",collection);

}

}


EventManager.cfc

component accessors="true" {

property configPath;

public any function init() {
variables.events = {};
variables.loaded = false;
}

public void function setConfigPath(required string configPath) {

if(fileExists(configPath)) {
variables.configPath = configPath;
}
else {
variables.configPath = expandPath(configPath);
}

}

public void function dispatchEvent(required string event, struct data) {

if(!structKeyExists(arguments,"data")) {
arguments.data = {};
}

if(!variables.loaded) {
loadConfig();
variables.loaded = true;
}

local.listeners = getListeners(event);

for (var i=1;i <= arrayLen(local.listeners);i++) {

local.bean = application.beanFactory.getBean(local.listeners[i].bean);

evaluate("local.bean.#local.listeners[i].method#(argumentCollection=data)");
}

}

private array function getListeners(required string event) {

if(!structKeyExists(variables.events,event)) {

local.used = {};

local.listeners = [];

for (var i=1;i <= arrayLen(variables.config);i++) {

if(reFindNoCase(variables.config[i].name,event)) {

for (var j=1;j <= arrayLen(variables.config[i].listeners);j++) {

local.listener = variables.config[i].listeners[j];

if(!structKeyExists(local.used,local.listener.id)) {

arrayAppend(local.listeners,local.listener);
local.used[local.listener.id] = true;

}

}

}

}

variables.events[event] = local.listeners;

}

return variables.events[event];

}

private void function loadConfig() {

variables.config = [];
local.xml = xmlParse(fileRead(getConfigPath()));

for (var i=1;i <= arrayLen(local.xml.events.xmlChildren);i++) {

local.event = {};
local.event.name = local.xml.events.xmlChildren[i].xmlAttributes.name;
local.event.listeners = [];

for (var j=1;j <= arrayLen(local.xml.events.xmlChildren[i].xmlChildren);j++) {

local.listener = {};
local.listener.bean = local.xml.events.xmlChildren[i].xmlChildren[j].xmlAttributes.bean;
local.listener.method = local.xml.events.xmlChildren[i].xmlChildren[j].xmlAttributes.method;
local.listener.id = local.listener.bean & "." & local.listener.method;

arrayAppend(local.event.listeners,local.listener);
}

arrayAppend(variables.config,local.event);

}

}

}


NotficationService.cfc

component {

public void function sendNotification() {

savecontent variable="local.body" {
writeDump(arguments)
}

var notification = new Mail();
notification.setTo("joe@example.com");
notification.setFrom("joe@example.com");
notification.setSubject("Notification");
notification.setType("html");
notification.send(body=local.body);

}

}


coldspring.xml

<beans>

<bean id="eventManager" class="sample.com.EventManager">
<property name="configPath">
<value>/sample/config/events.xml</value>
</property>
</bean>

<bean id="notificationService" class="sample.com.NotificationService" />

</beans>


events.xml

<events>
<event name="[\w]+:(pre|post)(Load|Insert|Update|Delete)">
<listener bean="notificationService" method="sendNotification" />
</event>
</events>


I'm not sure if this is the best approach or how well this would scale, but it seems to work pretty well for now.

PS - note the use of savecontent inside script. And they said it was pointless... :)

Tuesday, November 10, 2009

ColdFusion 9 Mail in cfscript

I ran into an issue today trying to send an email using script syntax. My code was pretty simple:


var email = new Mail();
email.setTo("joe@example.com");
email.setFrom("joe@example.com");
email.setSubject("Test Email");
email.setType("html");
email.send(body="Hello, world");


However, when I tried running the code, I got the following error:

Could not find the ColdFusion component or interface Mail.

While it's pretty obvious now what the problem is now, at first I was pretty confused. I figured since all the tags were converted to handle script syntax, everything should just work. I checked the ColdFusion 9 documentation and everything looked fine. After about 10 minutes I finally figured it out.

When installing ColdFusion, one of the first things I do is clean up ColdFusion Administrator by removing the default datasources and custom tag paths. However, when Adobe added script support for a couple tags (ftp, http, mail, pdf, query, storedproc), they chose to implement the functions as objects using CFCs. When I deleted the default custom tag path, ColdFusion was no longer able to find the Mail.cfc, since it was relying on the custom tag path.

If you go to cf_root\servers\cfusion\cfusion-ear\cfusion-war\WEB-INF\cfusion\CustomTags\com\adobe\coldfusion\, you should see all the tags implemented as CFCs. Kinda interesting.

On a random note, although it was said that <cfsavecontent /> would not be implemented in script syntax, apparently it was. You can see it in action if you view the examples on the documentation for using mail in script.


savecontent variable="mailBody"{
WriteOutput("This message was sent by...");
}