Getting Inherited CFC MetaData

Getting Inherited CFC MetaData

Posted by Brad Wood
May 06, 2012 08:19:00 UTC
ColdFusion's getMetaData function is a very handy way to introspect the functions, properties, and annotations of a component in an easy-to-digest format of arrays and structs. ColdFusion even nests structs to represent the metadata of the classes your component extends as well. Recently, for an enhancement I was building for WireBox, the DI/AOP framework inside ColdBox, I wanted to be able to condense all the metadata down for the component in question as well as all the components it inherits from to a single list of functions, properties, and annotations. After a short bit of Googling, I didn't find any code that did that so I decided it was a perfect opportunity to write a function that did just that.

Here is what the metadata looks like for a regular ColdFusion component. You can see that When that component extends another component, ColdFusion includes a key called "extends" in the struct which contains the metadata of the super class like so:

class1.cfc
[code]<cfcomponent displayName="class1" extends="class2" output="true" customAnnotation="class1 rocks">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfproperty name="name" default="Susan">
&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfproperty name="age">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cffunction name="getName" hint="overridden get for name">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cffunction>
&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cffunction name="getAge">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cffunction>
&nbsp;
</cfcomponent>
[/code]

class2.cfc
[code]<cfcomponent displayName="class2" output="false" customAnnotation="class2 rocks">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfproperty name="name" default="Bill">
&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cffunction name="getName" hint="base get for name">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cffunction>
&nbsp;
</cfcomponent>
[/code]


struct
CUSTOMANNOTATION class1 rocks
DISPLAYNAME class1
EXTENDS
struct
CUSTOMANNOTATION class2 rocks
DISPLAYNAME class2
EXTENDS
struct
FULLNAME WEB-INF.cftags.component
NAME WEB-INF.cftags.component
PATH C:\ColdFusion9\wwwroot\WEB-INF\cftags\component.cfc
TYPE component
FULLNAME class2
FUNCTIONS
array
1
struct
HINT base get for name
NAME getName
PARAMETERS
array [empty]
NAME class2
OUTPUT false
PATH D:\coldboxtest\class2.cfc
PROPERTIES
array
1
struct
DEFAULT Bill
NAME name
TYPE component
FULLNAME class1
FUNCTIONS
array
1
struct
NAME getAge
PARAMETERS
array [empty]
2
struct
HINT overridden get for name
NAME getName
PARAMETERS
array [empty]
NAME class1
OUTPUT true
PATH D:\coldboxtest\class1.cfc
PROPERTIES
array
1
struct
DEFAULT Susan
NAME name
2
struct
NAME age
TYPE component


This is a perfect use for our friend, recursion; which is when a function calls itself, creating a nested call stack of executions. Recursion uses more memory than iterative solutions, but usually results in simpler code.

I wrote a function called getInheritedMetadata(), which accepts either an instance of a CFC or a string which is a path to a CFC. That way, getInheritedMetadata() can take the place of both getMetaData() and getComponentMetaData(). The function starts by creating the familiar metadata struct that ColdFusion gives us, and keeps calling itself for each super class until it finds the top-most class in the inheritance chain. At that point, it starts building a new metadata struct (I didn't want overwrite CF's original metadata since it is cached.)

As the call stack unwinds and the function works its way back down the chain to the bottom class, it appends in the properties, functions, and annotations of each class overriding the super class just like real inheritance works. Once the function makes it to the end (back where it started), it has a new struct that represents the combined metadata of all the components in the inheritance tree. I also add an array into the combined metadata called inheritanceTrail which lists out all the classes that were combined together.

Here is the code:
[code]<cffunction name="getInheritedMetaData" output="false" hint="Returns a single-level metadata struct that includes all items inhereited from extending classes.">
&nbsp;&nbsp;&nbsp;<cfargument name="component" type="any" required="true" hint="A component instance, or the path to one">
&nbsp;&nbsp;&nbsp;<cfargument name="md" default="#structNew()#" hint="A structure containing a copy of the metadata for this level of recursion.">
&nbsp;&nbsp;&nbsp;<cfset var local = {}>
&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;<!--- First time through, get metaData of component. --->
&nbsp;&nbsp;&nbsp;<cfif structIsEmpty(md)>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfif isObject(component)>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset md = getMetaData(component)>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfelse>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset md = getComponentMetaData(component)>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cfif>
&nbsp;&nbsp;&nbsp;</cfif>
&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;<!--- If it has a parent, stop and calculate it first. --->
&nbsp;&nbsp;&nbsp;<cfif structKeyExists(md,"extends")>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.parent = getInheritedMetaData(component,md.extends)>
&nbsp;&nbsp;&nbsp;<!--- If we're at the end of the line, it's time to start working backwards so start with an empty struct to hold our condensesd metadata. --->
&nbsp;&nbsp;&nbsp;<cfelse>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.parent = {}>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.parent.inheritancetrail = []>
&nbsp;&nbsp;&nbsp;</cfif>
&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;<!--- Override ourselves into parent --->
&nbsp;&nbsp;&nbsp;<cfloop collection="#md#" item="local.key">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<!--- Functions and properties are an array of structs keyed on name, so I can treat them the same --->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfif listFind("FUNCTIONS,PROPERTIES",local.key)>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfif not structKeyExists(local.parent,local.key)>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.parent[local.key] = []>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cfif>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<!--- For each function/property in me... --->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfloop array="#md[local.key]#" index="local.item">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.parentItemCounter = 0>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.foundInParent = false>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<!--- ...Look for an item of the same name in my parent... --->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfloop array="#local.parent[local.key]#" index="local.parentItem">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.parentItemCounter++>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<!--- ...And override it --->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfif compareNoCase(local.item.name,local.parentItem.name) eq 0>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.parent[local.key][local.parentItemCounter] = local.item>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.foundInParent = true>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfbreak>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cfif>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cfloop>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<!--- ...Or add it --->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfif not local.foundInParent>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset arrayAppend(local.parent[local.key],local.item)>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cfif>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cfloop>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<!--- For everything else (component-level annotations), just override them directly into the parent --->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfelseif NOT listFind("EXTENDS,IMPLEMENTS",local.key)>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<cfset local.parent[local.key] = md[local.key]>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</cfif>
&nbsp;&nbsp;&nbsp;</cfloop>
&nbsp;&nbsp;&nbsp;<cfset arrayPrePend(local.parent.inheritanceTrail,local.parent.name)>
&nbsp;&nbsp;&nbsp;<cfreturn local.parent>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</cffunction>
[/code]


So, using the two components from above, here is the output from the getInheritedMetadata() function:

struct
CUSTOMANNOTATION class1 rocks
DISPLAYNAME class1
FULLNAME class1
FUNCTIONS
array
1
struct
HINT overridden get for name
NAME getName
PARAMETERS
array [empty]
2
struct
NAME getAge
PARAMETERS
array [empty]
INHERITANCETRAIL
array
1 class1
2 class2
3 WEB-INF.cftags.component
NAME class1
OUTPUT true
PATH D:\coldboxtest\class1.cfc
PROPERTIES
array
1
struct
DEFAULT Susan
NAME name
2
struct
NAME age
TYPE component


Download code here

John Allen

Very interesting and very cool.