Monday, January 29, 2007

Unlocking the power of CF Metadata

Lately I have been thinking an awful lot about metadata. We have this great metadata object in CF, but I rarely hear anything about it. Funny, even though we use metadata extensively in ColdSpring, I sort of forgot it was there for day to day use. ColdFusion's metadata object is free for the use in almost any way we want, and can provide extremely powerful class or method level annotations. Let's take a quick look at an example so you can see what I'm getting at. Write yourself any cfc, and write a simple cfm page to dump it's metadata:

<cfset obj = CreateObject('component','com.foo.mycfc').init() />
<cfdump var="#getMetadata(obj)#" />


This will output a big struct with all sorts of information about your object, like its name, extends, path, and lots of information its functions, like name, access, etc. There's a whole lot of great information for you to use, but what's really cool is, you can put your own stuff in there. Just make sure you don't try to overwrite any attributes that ColdFusion uses. This can be easily accomplished by using a prefix to create your own namespace. For my examples we'll use 'cs_' for our attributes. So lets add some custom metadata to our cfc. In the cfcomponent tag, lets add an attribute 'cs_metadata="really cool stuff"' (<cfcomponent name="mycfc" cs_metadata="really cool stuff"...) and run the cfm page we made in the previous example. Check out the output, now we have a cs_metadata key with the value "really cool stuff". Really cool, huh? Or are you thinking, so what? The thing that got me all excited about this is, if we use annotations on methods in our cfcs, we can access them in Advice components (you knew I was gonna get to some AOP right?), which is in fact really really cool stuff.

The great thing about AOP is it gives you the ability to develop abstract systems in an extremely generic manner. But when we apply them to specific components, we want them to take on more concrete functionality. The easiest way to do this is to provide your Aspects with parameters during configuration, enabling them to become more specialized to your components. For example, in the Klondike record store example I have been showing in my sessions, I have a generic caching system which can be configured for each type of component to cache by providing information to the Aspects. But the downfall of this is, I have to configure that Aspect over and over in ColdSpring for each model component. Enter metadata!

Let's take a look at a super simple system, one that always show up in any AOP conversation, logging. It's pretty easy to write a very general logging system. I like to wrap the logging in a service so we can reuse it anywhere, and provide log level methods, info(message), warn(message), error(message), debug(message), this way I can swap out cflog for log4j and control the log level in one place. Now all we need is an Aspect that will build a log statement from a method call. What if we would like more control, say to only include certain parameters for certain methods. This would be a nightmare if we tried to provide some sort of configuration to the Aspect, but we can easily put this info in the function metadata and read in in the Aspect! Here's how it's done, first we'll add some metadata to a cfc that we want to log method calls, notice I have added a cs_LogFields attribute:

<cfcomponent displayname="EntryService">

  ... init method and setters for dependencies ...

  <cffunction name="getEntriesByCategoryID" access="public" returntype="array" cs_LogFields="categoryID" output="false" >
    <cfargument name="categoryID" type="numeric" required="true" />
    <cfargument name="numToReturn" type="numeric" required="false" default="-1" />
    <cfargument name="activeOnly" type="boolean" required="false" default="true" />
  
    <cfreturn variables.entryGateway.getEntries(arguments.numToReturn, arguments.activeOnly, arguments.categoryID) />
  </cffunction>

  ... more service methods ...

</cfcomponent>


Next we have our logging service, I've used cflog in this example and added a setter for the log file name.

<cfcomponent displayname="LoggingService">

  <cffunction name="init" returntype="net.litepost.component.logging.LoggingService" access="public" output="false">
    <cfreturn this />
  </cffunction>

  <cffunction name="setFilename" access="public" returntype="void" output="false">
    <cfargument name="filename" type="string" required="true" />
    <cfset variables.filename = arguments.filename  />
  </cffunction>

  <cffunction name="info" access="public" returntype="void" output="false">
    <cfargument name="message" type="string" required="true" />
    <cflog file="#variables.filename#" type="information" text="#arguments.message#">
  </cffunction>

  ... rest of logging methods ...

</cfcomponent>


So now we will need an Advice class to tie it all together. I am going to use an AfterReturningAdvice for this one, it's going to be a bit more complex, so I'll put some comments in line.

<cfcomponent name="loggingAdvice" extends="coldspring.aop.AfterReturningAdvice">

  <!--- setters for the logging service --->
  <cffunction name="setLoggingService" returntype="void" access="public" output="false"
        hint="Dependency: security service">
    <cfargument name="loggingService" type="net.litepost.service.LoggingService" required="true"/>
    <cfset variables.m_loggingService = arguments.loggingService />
  </cffunction>

  <cffunction name="afterReturning" access="public" returntype="void">
    <cfargument name="returnVal" type="any" required="false" />
    <cfargument name="method" type="coldspring.aop.Method" required="false" />
    <cfargument name="args" type="struct" required="false" />
    <cfargument name="target" type="any" required="false" />
  
    <cfset var arg = '' />
    <cfset var argString = '' />
    <cfset var logFields = "" />
  
    <!--- first I need the metadata for the target object of the method call --->
    <cfset var objMd = getMetadata(arguments.target) />
    <cfset var objName = objMd.name />
  
    <!--- I also need the name of the method that was called --->
    <cfset var methodName = method.getMethodName() />

    <!--- I will use a private function to retrieve the metadata for that function --->
    <cfset var functionMd = getFunctionMetadata(objMd,methodName) />
  
    <!--- We use StructKeyExist to find our custom attribute, which we use as the logFields. If the attribute is missing, we are going to assume we want to log all the arguments to the method call --->
    <cfif StructKeyExists(functionMd, "cs_LogFields")>
      <cfset logFields = functionMd.cs_LogFields>
    <cfelse>
      <cfset logFields = StructKeyList(args)/>
    </cfif>
  
    <!--- now all we need to do is loop over our logFields list, and build out a string from those values (restricting to simpleValues) --->
    <cfloop list="#logFields#" index="arg">
      <cfif StructKeyExists(args, arg) and isSimpleValue(args[arg])>
        <cfif len(argString)>
          <cfset argString = argString & ', ' />
        </cfif>
        <cfset argString = argString & arg & '=' & args[arg] >
      </cfif>
    </cfloop>
  
    <!--- Now send it to the logging service! --->
    <cfset variables.m_loggingService.info("[" & objName & "] " & method.getMethodName() & "(" & argString & ") called!") />

  </cffunction>

  <!--- here's our private method to retrieve just a particular function metadata from the full metadata from a cfc --->
  <cffunction name="getFunctionMetadata" access="private" returntype="struct" output="false">
    <cfargument name="metadata" type="struct" required="true"/>
    <cfargument name="function" type="string" required="true"/>
    <cfset var ix = 0/>
    <cfset var mdFunctions = arguments.metadata.functions/>
  
    <cfloop from="1" to="#ArrayLen(mdFunctions)#" index="ix">
      <cfif mdFunctions[ix].name EQ arguments.function>
        <cfreturn mdFunctions[ix]/>
      </cfif>
    </cfloop>
    <cfreturn StructNew()/>
  
  </cffunction>

</cfcomponent>


If you've followed any of my previous AOP posts, you should have no problem setting all this up in your ColdSpring config file. I'll also include those links. This may look a bit complex at first sight, but look what we are gaining now. I could have provided setters to my advice class, allowing the application developer to configure it to specific uses, but that would mean they would need to configure this class over and over again for each different use, as I did for my caching system. By creating my advice class to read metadata, though, I have still provided that same customization, but this time with minimal work for the developers. Plus, we have provided the power of configuration right where the developer is working, in the cfcs themselves. This may not be the best way to go for all systems by any means, I still feel that the real power of ColdSpring AOP is its declarative approach, and it can be argued that this is going in the opposite direction. But the sheer power of this approach can open many doors to designing truly generic systems. For me that is the ultimate goal of AOP.

Happy programming, and I hope to see some readers at the frameworks conference where I plan to get a lot more in-depth into these concepts both in my session and drinking in the bar! See you!

Introduction to ColdSpring AOP
ColdSpring AOP Tutorial – Part One
ColdSpring AOP Tutorial – Part Two, Around Advice
Introducing auto-magic Remoting with ColdSpring

6 Comments:

Blogger Scott Arbeitman said...

You are not going far enough!

ColdFusion metadata allows us to remove a lot of the configuration of ColdSpring AOP altogether and use annotations (metadata) like you demonstrated.

This is, of course, the approach taken by Spring 2 for Java, where aspect-oriented annotations can be used instead of configuration files.

I've used this trick to great effect in many places, a lot of them taken from the Java world of annotations. Examples: in an MVC framework, link functions in your controller to events, and declare a function as a default event handler (Stripes). Persist a CFC in a database by annotating cfproperty tags, linking them to columns names and sql types, and the cfcomponent tag with the table name (Hibernate Annotations). Add validation to setter functions (Spring).

ColdFusion supports XML-style namespaces for custom attributes. I use "cs:" stype prefixes instead of cs_, since it seems more natural (although it really isn't).

1:49 AM  
Blogger Chris Scott said...

Oh I couldn't agree more! I'm just showing a simple example, hopefully getting people thinking. I use jpa annotations with hibernate at work, and we are looking to use xfire as well, which now supports WebService annotations. That's what actually got me thinking about annotations in cf, it's pretty amazing that we have such a simple implementation in cf medatada. I would even say, it would be a major benefit if cf would provide even easier access method.

As you said with the xml-style namespaces, you can use anything, and I agree probably a better style as far as best practices would be concerned

4:46 AM  
Blogger Scott Arbeitman said...

I also wanted to add that it makes sense to use a namespace prefix for a cfc that corresponds to a mapping that would make your cfc accessible. This is basically the first level package name.

So "coldspring" is really preferable to "cs", sice it won't clash with any future framework which leverages metadata in this way.

4:58 AM  
Anonymous Chip Temm said...

Hi
I also wrote an article on extending metadata here: http://coldfusion.sys-con.com/read/311292.htm

May be of interest,
Chip

6:37 PM  
Anonymous Mark Mandel said...

Just thinking about it.. you could probably have also gone:

cfset var functionMd = getMetadata(arguments.target[methodName])

and I believe it would have worked, rather than being forced to loop around the metadata.

I know you can run getMetaData on a function directly, and it will happily return you the meta data for just that function.. and since a CFC is just a map... it should all work out nicely.

Interesting article :D

4:38 PM  
Anonymous Anonymous said...

Hi,

thx for your article, but one thing I've to criticize:
black background and white font

Sorry, but it is horrible to read.

It's great to have the FF-extension firebug, because with that you're able to change CSS

3:22 PM  

Post a Comment

<< Home